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

Side by Side Diff: ios/chrome/browser/ui/stack_view/stack_view_controller.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 2012 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/stack_view/stack_view_controller.h"
6
7 #import <QuartzCore/QuartzCore.h>
8
9 #include <algorithm>
10 #include <cmath>
11 #include <limits>
12
13 #include "base/format_macros.h"
14 #import "base/ios/block_types.h"
15 #import "base/ios/weak_nsobject.h"
16 #include "base/logging.h"
17 #import "base/mac/bundle_locations.h"
18 #import "base/mac/foundation_util.h"
19 #import "base/mac/objc_property_releaser.h"
20 #include "base/mac/scoped_block.h"
21 #import "base/mac/scoped_nsobject.h"
22 #include "base/metrics/histogram.h"
23 #include "base/metrics/user_metrics.h"
24 #include "base/metrics/user_metrics_action.h"
25 #include "base/strings/sys_string_conversions.h"
26 #include "ios/chrome/browser/chrome_url_constants.h"
27 #include "ios/chrome/browser/experimental_flags.h"
28 #import "ios/chrome/browser/tabs/tab.h"
29 #import "ios/chrome/browser/tabs/tab_model.h"
30 #import "ios/chrome/browser/tabs/tab_model_observer.h"
31 #import "ios/chrome/browser/ui/animation_util.h"
32 #import "ios/chrome/browser/ui/background_generator.h"
33 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
34 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
35 #include "ios/chrome/browser/ui/commands/ios_command_ids.h"
36 #import "ios/chrome/browser/ui/keyboard/UIKeyCommand+Chrome.h"
37 #import "ios/chrome/browser/ui/ntp/new_tab_page_toolbar_controller.h"
38 #import "ios/chrome/browser/ui/reversed_animation.h"
39 #import "ios/chrome/browser/ui/rtl_geometry.h"
40 #import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h"
41 #import "ios/chrome/browser/ui/stack_view/card_stack_pinch_gesture_recognizer.h"
42 #import "ios/chrome/browser/ui/stack_view/card_view.h"
43 #import "ios/chrome/browser/ui/stack_view/close_button.h"
44 #import "ios/chrome/browser/ui/stack_view/page_animation_util.h"
45 #import "ios/chrome/browser/ui/stack_view/stack_card.h"
46 #import "ios/chrome/browser/ui/stack_view/stack_view_controller_private.h"
47 #import "ios/chrome/browser/ui/stack_view/stack_view_toolbar_controller.h"
48 #import "ios/chrome/browser/ui/stack_view/title_label.h"
49 #import "ios/chrome/browser/ui/toolbar/new_tab_button.h"
50 #import "ios/chrome/browser/ui/toolbar/toolbar_owner.h"
51 #import "ios/chrome/browser/ui/tools_menu/tools_menu_context.h"
52 #import "ios/chrome/browser/ui/tools_menu/tools_menu_view_item.h"
53 #import "ios/chrome/browser/ui/ui_util.h"
54 #import "ios/chrome/browser/ui/uikit_ui_util.h"
55 #import "ios/chrome/common/material_timing.h"
56 #include "ios/chrome/grit/ios_strings.h"
57 #include "ios/web/public/referrer.h"
58 #import "net/base/mac/url_conversions.h"
59 #include "ui/base/l10n/l10n_util.h"
60
61 using base::UserMetricsAction;
62
63 // To obtain scroll behavior, places the card stacks' display views within a
64 // UIScrollView container. The container is used only as a driver of scroll
65 // events. To avoid the finite size of the container from impacting scrolling,
66 // (1) the container is made large enough that it cannot be scrolled to a
67 // boundary point without the user having first fully scrolled the card stack
68 // in that direction, and (2) after scroll events, the container's scroll
69 // offset is recentered if necessary.
70
71 namespace {
72 // The fraction of the display to use for the active deck if both decks are
73 // being displayed.
74 const CGFloat kActiveDeckDisplayFraction = 0.85;
75 // Animation durations.
76 const NSTimeInterval kCascadingCardCloseDelay = 0.1;
77 const NSTimeInterval kDefaultAnimationDuration = 0.25;
78 // The length of the animation that eliminates overextension after a
79 // scroll/pinch.
80 const NSTimeInterval kOverextensionEliminationAnimationDuration = .4;
81 // Fraction of the screen that must be swiped for a stack to switch or a card
82 // to dismiss.
83 const CGFloat kSwipeCardScreenFraction = 0.35;
84 // The velocity (in points / millisecond) below which a scroll's deceleration
85 // will be killed once the stack is overscrolled. Determined by experimentation
86 // to see what resulted in a good feel.
87 const CGFloat kMinFlingVelocityInOverscroll = 0.3;
88 // The velocity (in points / millisecond) at/above which a scroll will be
89 // treated as a fling even if it is not for the purposes of determining
90 // overscroll behavior. Used to handle the corner case where the user flings
91 // the cards toward the start stack, but at the moment that the scrolled card
92 // would become overscrolled, the finger is still registered as tracking. Has
93 // the tradeoff that if user is legimately scrolling above this velocity near
94 // the start stack, the card will not track the user's finger. Value determined
95 // by experimentation to see what resulted in a good handling of this tradeoff.
96 const CGFloat kThresholdVelocityForTreatingScrollAsFling = 1.0;
97 // The factor by which scroll velocity is decayed once a fling becomes
98 // overscrolled.
99 const CGFloat kDecayFactorInBounce = .75;
100 // The duration (in seconds) that the user must press on a card before
101 // beginning an ambiguous swipe (i.e., a swipe that could result in either
102 // dismissing the card or changing decks) for that swipe to trigger card
103 // dismissal instead of changing decks.
104 const CGFloat kPressDurationForAmbiguousSwipeToTriggerDismissal = .2;
105 // The vertical overlap between the scroll view and the toolbar (chosen to match
106 // aspect ratio of snapshotted webview).
107 const CGFloat kVerticalToolbarOverlap = 0.0;
108 // The delay into the dismissal transition animation at which to update the
109 // status bar. The value was provided by UX, and corresponds to approximately
110 // when the selected card's frame image crosses the status bar in the animation.
111 const NSTimeInterval kDismissalStatusBarUpdateDelay = 0.15;
112 // When choosing the size of the cards, ensure that the bottom of the card
113 // is at most |kCardBottomPadding| from the bottom of the scroll view.
114 const CGFloat kCardBottomPadding = 29.0;
115 // Animation key used for the dummy toolbar background view animation.
116 NSString* const kDummyToolbarBackgroundViewAnimationKey =
117 @"DummyToolbarBackgroundViewAnimationKey";
118 } // anonymous namespace
119
120 // Container for the state associated with gesture-related events.
121 @interface GestureStateTracker : NSObject
122
123 @property(nonatomic, assign) NSUInteger scrollCardIndex;
124 @property(nonatomic, assign) CGFloat previousScrollOffset;
125 @property(nonatomic, assign) base::TimeTicks previousScrollTime;
126 // The current scroll velocity, in points / millisecond.
127 @property(nonatomic, readonly) CGFloat scrollVelocity;
128 @property(nonatomic, assign) BOOL resetScrollCardOnNextDrag;
129 @property(nonatomic, assign) NSUInteger firstPinchCardIndex;
130 @property(nonatomic, assign) NSUInteger secondPinchCardIndex;
131 @property(nonatomic, assign) CGFloat previousFirstPinchOffset;
132 @property(nonatomic, assign) CGFloat previousSecondPinchOffset;
133 // YES when a pinch gesture is currently being recognized.
134 @property(nonatomic, assign) BOOL pinching;
135 // YES when a 1-fingered pinch gesture is being interpreted by
136 // StackViewController's |handlePinchFrom:| as a scroll.
137 @property(nonatomic, assign) BOOL scrollingInPinch;
138 // Swipe gesture starting position. In portrait, this is the x position of the
139 // beginning touch. In landscape this is the y position.
140 @property(nonatomic, assign) CGFloat swipeStartingPosition;
141 // If YES, a swipe gesture is interpreted as being a swipe to change decks.
142 // Otherwise, a swipe gesture is interpreted as being a swipe to close a card.
143 @property(nonatomic, assign) BOOL swipeChangesDecks;
144 // The index of the card being swiped. Undefined if |swipeChangesDecks| is YES.
145 @property(nonatomic, assign) NSUInteger swipedCardIndex;
146 @property(nonatomic, assign) BOOL resetSwipedCardOnNextSwipe;
147 @property(nonatomic, assign) BOOL swipeIsBeginning;
148 // Whether a swipe whose intent is ambiguous should change decks (as opposed to
149 // dismiss a card). Relevant only when multiple stacks are present.
150 @property(nonatomic, assign) BOOL ambiguousSwipeChangesDecks;
151
152 // Given |distance|, which should be the distance scrolled since
153 // |previousScrollTime|, updates |scrollVelocity|.
154 - (void)updateScrollVelocityWithScrollDistance:(CGFloat)distance;
155
156 @end
157
158 @implementation GestureStateTracker
159
160 @synthesize ambiguousSwipeChangesDecks = _ambiguousSwipeChangesDecks;
161 @synthesize firstPinchCardIndex = _firstPinchCardIndex;
162 @synthesize pinching = _pinching;
163 @synthesize previousFirstPinchOffset = _previousFirstPinchOffset;
164 @synthesize previousScrollOffset = _previousScrollOffset;
165 @synthesize previousScrollTime = _previousScrollTime;
166 @synthesize previousSecondPinchOffset = _previousSecondPinchOffset;
167 @synthesize resetScrollCardOnNextDrag = _resetScrollCardOnNextDrag;
168 @synthesize resetSwipedCardOnNextSwipe = _resetSwipedCardOnNextSwipe;
169 @synthesize scrollCardIndex = _scrollCardIndex;
170 @synthesize scrollingInPinch = _scrollingInPinch;
171 @synthesize scrollVelocity = _scrollVelocity;
172 @synthesize secondPinchCardIndex = _secondPinchCardIndex;
173 @synthesize swipeChangesDecks = _swipeChangesDecks;
174 @synthesize swipedCardIndex = _swipedCardIndex;
175 @synthesize swipeIsBeginning = _swipeIsBeginning;
176 @synthesize swipeStartingPosition = _swipeStartingPosition;
177
178 - (instancetype)init {
179 if ((self = [super init])) {
180 _resetScrollCardOnNextDrag = YES;
181 }
182 return self;
183 }
184
185 - (void)updateScrollVelocityWithScrollDistance:(CGFloat)distance {
186 base::TimeDelta elapsedTime = base::TimeTicks::Now() - _previousScrollTime;
187 if (elapsedTime == base::TimeDelta::FromMicroseconds(0))
188 return;
189 _scrollVelocity =
190 fabs(distance / CGFloat(elapsedTime.InMillisecondsRoundedUp()));
191 }
192
193 @end
194
195 @interface StackViewController (Private)
196
197 // Clears the internal state of the object. Should only be called when the
198 // object is not being shown. After this method is called, a call to
199 // |restoreInternalState| must be made before the object is reshown.
200 - (void)clearInternalState;
201 // Updates the layout parameters of the scroll view and the display views based
202 // on the viewport size. Should be called any time that the viewport size is
203 // changed.
204 - (void)viewportSizeWasChanged;
205 // Configures the scroll view to be large enough so that the user could not
206 // scroll to one of its boundaries without also having reached the
207 // corresponding boundary of the stack being scrolled.
208 - (void)updateScrollViewContentSize;
209 // Deregisters for the notifications |registerForNotifications| specifies.
210 - (void)deregisterForNotifications;
211 // Eliminates the ability for the user to perform any further interactions
212 // with the stack view. Should be called when the stack view starts being
213 // dismissed.
214 - (void)prepareForDismissal;
215
216 // Asynchronously adds the remaining cards to the display view to pre-load them.
217 // This is safe to call multiple times.
218 - (void)preloadCardViewsAsynchronously;
219 // Adds the next not-yet-loaded card to the display view.
220 - (void)preloadNextCardView;
221 // Animates the removal of |cardView| from the superview, with the start of the
222 // animation being delayed by |delay|. Performs |completion| on animation
223 // finish (may be NULL). Card will dismiss clockwise when |clockwise| is YES and
224 // counter-clockwise when |clockwise| is NO.
225 - (void)animateOutCardView:(CardView*)cardView
226 delay:(NSTimeInterval)delay
227 clockwise:(BOOL)clockwise
228 completion:(ProceduralBlock)completion;
229 // Removes all cards in |cardSet| from the underlying model and their superview.
230 - (void)removeAllCardsFromSet:(CardSet*)cardSet;
231 // Disable all the gesture handlers. Must be called before removing cards from
232 // the active set.
233 - (void)disableGestureHandlers;
234 // Enable all the gesture handlers.
235 - (void)enableGestureHandlers;
236 // Should be called whenever the number of cards in the active set changes
237 // (including the active set itself changing).
238 - (void)activeCardCountChanged;
239 // Computes and stores the initial card size information that will decide how
240 // layout is done for the remainder of the stack view's lifetime, and configures
241 // the card sets accordingly.
242 - (void)setInitialCardSizing;
243 // Updates the card sizing and layout for the current device orientation.
244 // If |animates| is true, the size change will be animated, otherwise it will be
245 // done synchronously.
246 - (void)updateDeckOrientationWithAnimation:(BOOL)animates;
247 // Updates the card sizing for the current deck states and device orientation.
248 // If |animates| is true, the size change will be animated, otherwise it will be
249 // done synchronously.
250 - (void)updateCardSizesWithAnimation:(BOOL)animates;
251 // Animates setting the opacity of the card tabs of the current deck to
252 // |opacity|.
253 - (void)animateActiveSetCardTabsToOpacity:(CGFloat)opacity
254 withDuration:(CGFloat)duration
255 completion:(ProceduralBlock)completion;
256 // Updates the positions of the decks in the non-layout direction. Should be
257 // called if the layout direction, card size, or active set changes.
258 - (void)updateDeckAxisPositions;
259 // Updates the positions of the decks in the non-layout direction and then
260 // shifts them from the standard positions by |amount|.
261 - (void)updateDeckAxisPositionsWithShiftAmount:(CGFloat)amount;
262 // Updates the position of |cardSet| in the non-layout direction and then
263 // shifts it from its standard position by |amount|.
264 - (void)updateDeckAxisPositionForCardSet:(CardSet*)cardSet
265 withShiftAmount:(CGFloat)shiftAmount;
266 // The amount by which |cardSet| should be shifted from its default layout axis
267 // position to be positioned offscreen.
268 - (CGFloat)shiftOffscreenAmountForCardSet:(CardSet*)cardSet;
269 // Refreshes the card display, using the current orientation. Should be called
270 // any time the orientation has changed or the display views have been rebuilt.
271 - (void)refreshCardDisplayWithAnimation:(BOOL)animates;
272 // Updates the UI to reflect the current card set. Called automatically by
273 // setActiveCardSet:, but also called during setup.
274 - (void)displayActiveCardSet;
275 // Switches to showing only the main card set, with no room for showing the
276 // incognito set. Should be called when the last incognito card is closed.
277 - (void)displayMainCardSetOnly;
278 // Updates the appearance of the toolbar for the current state of the card
279 // stack.
280 - (void)updateToolbarAppearanceWithAnimation:(BOOL)animate;
281
282 // Returns the size of a single card (at normal zoom).
283 - (CGSize)cardSize;
284 // Returns the size that should be used for cards being displayed in a viewport
285 // with breadth |breadth|, taking margins into account and preserving the
286 // content area aspect ratio.
287 - (CGSize)cardSizeForBreadth:(CGFloat)breadth;
288 // Returns the amount that |point| is offset on the current scroll axis.
289 - (CGFloat)scrollOffsetAmountForPoint:(CGPoint)point;
290 // Returns the amount that |position| is offset on the current scroll axis.
291 - (CGFloat)scrollOffsetAmountForPosition:(LayoutRectPosition)position;
292 // Returns the CGPoint offset |offset| in the current scroll direction, with
293 // 0 as the other component of the point.
294 - (CGPoint)scrollOffsetPointWithAmount:(CGFloat)offset;
295 // Returns the length of |size| in the current scroll direction.
296 - (CGFloat)scrollLength:(CGSize)size;
297 // Returns the length of |size| in the non-scroll direction.
298 - (CGFloat)scrollBreadth:(CGSize)size;
299 // Returns a size for the current scroll direction with the given scroll length
300 // and breadth.
301 - (CGSize)sizeForScrollLength:(CGFloat)length breadth:(CGFloat)breadth;
302 // Returns the index of the card that |point| is contained within, or
303 // |NSNotFound| if |point| is not contained in any card. This is not an
304 // efficient lookup, so this should *not* be called frequently.
305 - (NSUInteger)indexOfCardAtPoint:(CGPoint)point;
306 // Will reverse the current transition animations if the tab switcher button is
307 // pressed before the animation can finish.
308 - (void)cancelTransitionAnimation;
309 // Called within the completion block for the transition animations.
310 - (void)finishTransitionAnimation;
311 // Called within the completion block for the transition animations. Notifies
312 // the delegates that the current transition has finished.
313 - (void)notifyDelegatesTransitionFinished;
314 // Sets up the view hierarchy for transitioning and calls the stack view and
315 // toolbar animation selectors below, wrapping the animations in a single
316 // CATransaction. Use |transitionStyle| = StackTransitionStylePresenting for
317 // presentation and |transitionStyle| = StackTransitionStyleDismissing for
318 // dismissal.
319 - (void)animateTransitionWithStyle:(StackTransitionStyle)transitionStyle;
320 // Updates the view heirarchy for the transition based on the current transition
321 // style. If the style is STACK_TRANSITION_STYLE_PRESENTING or
322 // STACK_TRANSITION_STYLE_DISMISSING, the display views are added to the root
323 // view and the toolbar is inserted between them. If the style is
324 // STACK_TRANSITION_STYLE_NONE, the display views are reparented into the scroll
325 // view and aligned to the view port.
326 - (void)reorderSubviewsForTransition;
327 // Animates the cards in |cardSet| from |beginLayouts| to |endLayouts| with the
328 // transition style specified by |self.transitionStyle|.
329 - (void)animateCardSet:(CardSet*)cardSet
330 fromBeginLayouts:(std::vector<LayoutRect>)beginLayouts
331 toEndLayouts:(std::vector<LayoutRect>)endLayouts;
332 // Reverses the cancelled transition animations added to the card set.
333 - (void)reverseTransitionAnimationsForCardSet:(CardSet*)cardSet;
334 // Adds transition animations to the active card set. If |self.transitionStyle|
335 // = StackTransitionStylePresenting, this will fan out the cards in the active
336 // set from the frames returned by |-cardTransitionFrames| to the fanned out
337 // frames. If |transitionStyle| = StackTransitionStyleDismissing, the cards will
338 // animate from their current frames to |-cardTransitionFrames|.
339 - (void)animateActiveSetTransition;
340 // Adds transition animations to the inactive set. The inactive card set will
341 // be laid out in the fanned frames and will animate in from offscreen if
342 // |self.transitionStyle| = StackTransitionStylePresenting, and will animate off
343 // the screen if |transitionStyle| = StackTransitionStyleDismissing.
344 - (void)animateInactiveSetTransition;
345 // Adds the dummy toolbar background to the active card set's current card and
346 // animates alongside the toolbar, creating a cross-fade effect from the
347 // toolbar's frame at the top of the screen to the tab portion of |cardFrame|
348 // and vise versa.
349 - (void)animateDummyToolbarForCardFrame:(CGRect)cardFrame
350 transitionStyle:(StackTransitionStyle)transitionStyle;
351 // Reverses the dummy toolbar background animations for cancelled transitions.
352 - (void)reverseDummyToolbarBackgroundViewAnimation;
353 // Adds the toolbar view owned by |transitionToolbarController| to the stack's
354 // view hierarchy and animates the frame alongside the active card set's current
355 // card (i.e. from its position at the top of the screen to the tab portion of
356 // the provided |cardFrame| or vise versa). The transition completion delegate
357 // callbacks will reparent the toolbar view back into the BVC's view hiearchy.
358 - (void)animateTransitionToolbarWithCardFrame:(CGRect)cardFrame
359 transitionStyle:
360 (StackTransitionStyle)transitionStyle;
361 // Returns an vector of LayoutRects corresponding to the active card set's
362 // frames at the moment of switching to or from the stack view. The cards below
363 // the current card in the z axis will have top-aligned unscaled frames, the
364 // current card will have a top-aligned frame scaled such that the web view
365 // snapshot takes the entire screen below the toolbar, and cards above the the
366 // current card will have the scaled frame translated offscreen.
367 - (std::vector<LayoutRect>)cardTransitionLayouts;
368 // Returns the index of what should be the first visible card in the initial
369 // fanout of |cardSet|.
370 - (NSUInteger)startIndexOfInitialFanoutForCardSet:(CardSet*)cardSet;
371 // Perform the animation for switching out of the stack view while
372 // simultaneously opening a tab with |url|, at |position| with the given
373 // |transition|. The tab where |url| is opened is returned.
374 - (Tab*)dismissWithNewTabAnimation:(const GURL&)url
375 atIndex:(NSUInteger)position
376 transition:(ui::PageTransition)transition;
377 - (void)closeTab:(id)sender;
378 - (void)handleLongPressFrom:(UIPinchGestureRecognizer*)recognizer;
379 - (void)handlePinchFrom:(UIPinchGestureRecognizer*)recognizer;
380 - (void)handleTapFrom:(UITapGestureRecognizer*)recognizer;
381 // Returns the card corresponding to |view|. This is not an efficient lookup,
382 // so this should *not* be called frequently.
383 - (StackCard*)cardForView:(CardView*)view;
384 // Shows the tools menu popup.
385 - (void)showToolsMenuPopup;
386 // All local tab state is cleared, and |currentlyClosingAllTabs_| is set to
387 // |NO|.
388 - (void)allModelTabsHaveClosed:(NSNotification*)notify;
389
390 // Updates the display views so that they are aligned to the scroll view's
391 // viewport. Should be called any time the scroll view's content offset
392 // changes.
393 - (void)alignDisplayViewsToViewport;
394
395 // Handles swipe gestures between card sets and swipes to remove cards.
396 - (void)handlePanFrom:(UIPanGestureRecognizer*)gesture;
397 // Determines whether the current swipe should be treated as a swipe to dismiss
398 // a card or a swipe to change decks.
399 - (void)determineSwipeType:(UIPanGestureRecognizer*)gesture;
400 // Returns the distance that a swipe needs to travel in order to trigger an
401 // action (close card/change deck).
402 - (CGFloat)distanceForSwipeToTriggerAction;
403 // Returns |YES| if the current swipe should trigger an action (close
404 // card/change deck) based on the swipe's ending position and its starting
405 // position.
406 - (BOOL)swipeShouldTriggerAction:(CGFloat)endingPosition;
407 // Moves between card sets, potentially changing the active card set at the end
408 // of the gesture.
409 - (void)swipeDeck:(UIPanGestureRecognizer*)gesture;
410 // Moves the card being swiped, potentially dismissing the card at the end of
411 // the gesture.
412 - (void)swipeCard:(UIPanGestureRecognizer*)gesture;
413
414 // Returns whether the current scroll should be ended.
415 - (BOOL)shouldEndScroll;
416 // Performs any necessary cleanup actions after a scroll is completed.
417 - (void)scrollEnded;
418 // Performs any necessary cleanup actions after a pinch is completed.
419 - (void)pinchEnded;
420 // Animates overextension elimination from the active card set. Performs
421 // |completion| on animation finish (may be |NULL|).
422 - (void)animateOverextensionEliminationWithCompletion:
423 (ProceduralBlock)completion;
424 // Cancels a scroll that is in its deceleration phase.
425 // NOTE: Will not have the desired behavior if invoked on a scroll that is not
426 // in the deceleration phase, i.e., the user is still dragging on the screen.
427 - (void)killScrollDeceleration;
428 // Adjusts the amount that the stack is allowed to overextend depending on
429 // whether the current scroll is a fling.
430 - (void)adjustMaximumOverextensionAmount:(BOOL)isFling;
431
432 // Responds to voice over focusing on TitleLabel or CloseButton. If the element
433 // label is covered, scroll it toward the start stack, or the next card toward
434 // the end stack as appropriate. If the element is in the middle of a stack, fan
435 // outcards from its card's index.
436 - (void)accessibilityFocusedOnElement:(id)element;
437
438 // Determine the center of |sender| if it's a view or a toolbar item and store.
439 - (void)setLastTapPoint:(id)sender;
440
441 @end
442
443 @implementation StackViewController {
444 base::scoped_nsobject<UIScrollView> _scrollView;
445 // The view containing the stack view's background.
446 base::scoped_nsobject<UIView> _backgroundView;
447 // The main card set.
448 base::scoped_nsobject<CardSet> _mainCardSet;
449 // The off-the-record card set.
450 base::scoped_nsobject<CardSet> _otrCardSet;
451 // The currently active card set; one of _mainCardSet or _otrCardSet.
452 CardSet* _activeCardSet; // weak
453 id<TabSwitcherDelegate> _delegate; // weak
454 id<StackViewControllerTestDelegate> _testDelegate; // weak
455 // Controller for the stack view toolbar.
456 base::scoped_nsobject<StackViewToolbarController> _toolbarController;
457 // The size of a card at the time the stack was first shown.
458 CGSize _initialCardSize;
459 // The previous orientation of the interface.
460 UIInterfaceOrientation _lastInterfaceOrientation;
461 // Gesture recognizer to catch taps on the inactive stack.
462 base::scoped_nsobject<UITapGestureRecognizer> _modeSwitchRecognizer;
463 // Gesture recognizer to catch pinches in the active scroll view.
464 base::scoped_nsobject<UIGestureRecognizer> _pinchRecognizer;
465 // Gesture recognizer to catch swipes to switch decks/dismiss cards.
466 base::scoped_nsobject<UIGestureRecognizer> _swipeGestureRecognizer;
467 // Gesture recognizer that determines whether an ambiguous swipe action
468 // (i.e., a swipe on an active card in the direction that would cause a deck
469 // change) should trigger a change of decks or a card dismissal.
470 base::scoped_nsobject<UILongPressGestureRecognizer>
471 _swipeDismissesCardRecognizer;
472 // Tracks the parameters of gesture-related events.
473 base::scoped_nsobject<GestureStateTracker> _gestureStateTracker;
474 // If |YES|, callbacks to |scrollViewDidScroll:| do not trigger scrolling.
475 // Default is |NO|.
476 BOOL _ignoreScrollCallbacks;
477 // The scroll view's pan gesture recognizer.
478 UIPanGestureRecognizer* _scrollGestureRecognizer; // weak
479 // Because the removal of the StackCard during a swipe happens in a callback,
480 // track which direction the animation should dismiss with.
481 // |_reverseDismissCard| is only set when the dismissal happens in reverse.
482 base::scoped_nsobject<StackCard> _reverseDismissCard;
483 // |YES| if the stack view is in the process of being dismissed.
484 BOOL _isBeingDismissed;
485 // |YES| if the stack view is currently active.
486 BOOL _isActive;
487 // Records whether a memory warning occurred in the current session.
488 BOOL _receivedMemoryWarningInSession;
489 // |YES| if there is card set animation being processed. For testing only.
490 // Save last touch point used by new tab animation.
491 CGPoint _lastTapPoint;
492
493 base::mac::ObjCPropertyReleaser _propertyReleaserStackViewController;
494 }
495
496 @synthesize activeCardSet = _activeCardSet;
497 @synthesize delegate = _delegate;
498 @synthesize dummyToolbarBackgroundView = _dummyToolbarBackgroundView;
499 @synthesize inActiveDeckChangeAnimation = _inActiveDeckChangeAnimation;
500 @synthesize testDelegate = _testDelegate;
501 @synthesize transitionStyle = _transitionStyle;
502 @synthesize transitionTappedCard = _transitionTappedCard;
503 @synthesize transitionToolbarController = _transitionToolbarController;
504 @synthesize transitionToolbarFrame = _transitionToolbarFrame;
505 @synthesize transitionToolbarOwner = _transitionToolbarOwner;
506 @synthesize transitionWasCancelled = _transitionWasCancelled;
507
508 - (instancetype)initWithMainCardSet:(CardSet*)mainCardSet
509 otrCardSet:(CardSet*)otrCardSet
510 activeCardSet:(CardSet*)activeCardSet {
511 DCHECK(mainCardSet);
512 DCHECK(otrCardSet);
513 DCHECK(activeCardSet == otrCardSet || activeCardSet == mainCardSet);
514 self = [super initWithNibName:nil bundle:nil];
515 if (self) {
516 _propertyReleaserStackViewController.Init(self,
517 [StackViewController class]);
518 [self setUpWithMainCardSet:mainCardSet
519 otrCardSet:otrCardSet
520 activeCardSet:activeCardSet];
521 _swipeDismissesCardRecognizer.reset([[UILongPressGestureRecognizer alloc]
522 initWithTarget:self
523 action:@selector(handleLongPressFrom:)]);
524 [_swipeDismissesCardRecognizer
525 setMinimumPressDuration:
526 kPressDurationForAmbiguousSwipeToTriggerDismissal];
527 [_swipeDismissesCardRecognizer setDelegate:self];
528 _pinchRecognizer.reset([[CardStackPinchGestureRecognizer alloc]
529 initWithTarget:self
530 action:@selector(handlePinchFrom:)]);
531 [_pinchRecognizer setDelegate:self];
532 _modeSwitchRecognizer.reset([[UITapGestureRecognizer alloc]
533 initWithTarget:self
534 action:@selector(handleTapFrom:)]);
535 [_modeSwitchRecognizer setDelegate:self];
536 }
537 return self;
538 }
539
540 - (instancetype)initWithMainTabModel:(TabModel*)mainModel
541 otrTabModel:(TabModel*)otrModel
542 activeTabModel:(TabModel*)activeModel {
543 DCHECK(mainModel);
544 DCHECK(otrModel);
545 DCHECK(activeModel == otrModel || activeModel == mainModel);
546 base::scoped_nsobject<CardSet> mainCardSet(
547 [[CardSet alloc] initWithModel:mainModel]);
548 base::scoped_nsobject<CardSet> otrCardSet(
549 [[CardSet alloc] initWithModel:otrModel]);
550 CardSet* activeCardSet =
551 (activeModel == mainModel) ? mainCardSet.get() : otrCardSet.get();
552 return [self initWithMainCardSet:mainCardSet
553 otrCardSet:otrCardSet
554 activeCardSet:activeCardSet];
555 }
556
557 - (instancetype)initWithNibName:(NSString*)nibNameOrNil
558 bundle:(NSBundle*)nibBundleOrNil {
559 NOTREACHED();
560 return nil;
561 }
562
563 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
564 NOTREACHED();
565 return nil;
566 }
567
568 - (void)setUpWithMainCardSet:(CardSet*)mainCardSet
569 otrCardSet:(CardSet*)otrCardSet
570 activeCardSet:(CardSet*)activeCardSet {
571 _mainCardSet.reset([mainCardSet retain]);
572 _otrCardSet.reset([otrCardSet retain]);
573 if (experimental_flags::IsLRUSnapshotCacheEnabled()) {
574 [_mainCardSet setKeepOnlyVisibleCardViewsAlive:YES];
575 [_otrCardSet setKeepOnlyVisibleCardViewsAlive:YES];
576 }
577 _activeCardSet = (activeCardSet == mainCardSet) ? mainCardSet : otrCardSet;
578 _gestureStateTracker.reset([[GestureStateTracker alloc] init]);
579 _pinchRecognizer.reset([[CardStackPinchGestureRecognizer alloc]
580 initWithTarget:self
581 action:@selector(handlePinchFrom:)]);
582 [_pinchRecognizer setDelegate:self];
583 _modeSwitchRecognizer.reset([[UITapGestureRecognizer alloc]
584 initWithTarget:self
585 action:@selector(handleTapFrom:)]);
586 [_modeSwitchRecognizer setDelegate:self];
587 }
588
589 - (void)restoreInternalStateWithMainTabModel:(TabModel*)mainModel
590 otrTabModel:(TabModel*)otrModel
591 activeTabModel:(TabModel*)activeModel {
592 DCHECK(mainModel);
593 DCHECK(otrModel);
594 DCHECK(activeModel == otrModel || activeModel == mainModel);
595 DCHECK(!_isActive);
596 base::scoped_nsobject<CardSet> mainCardSet(
597 [[CardSet alloc] initWithModel:mainModel]);
598 base::scoped_nsobject<CardSet> otrCardSet(
599 [[CardSet alloc] initWithModel:otrModel]);
600 CardSet* activeCardSet =
601 (activeModel == mainModel) ? mainCardSet.get() : otrCardSet.get();
602 [self setUpWithMainCardSet:mainCardSet
603 otrCardSet:otrCardSet
604 activeCardSet:activeCardSet];
605
606 // If the view is not currently loaded, do not adjust its size or add
607 // gesture recognizers. That work will be done in |viewDidLoad|.
608 if ([self isViewLoaded]) {
609 [self prepareForDisplay];
610 // The delegate is set to nil when the stack view is dismissed.
611 [_scrollView setDelegate:self];
612 }
613 }
614
615 - (void)setOtrTabModel:(TabModel*)otrModel {
616 DCHECK(_isActive);
617 DCHECK(_mainCardSet == _activeCardSet);
618 DCHECK([otrModel count] == 0);
619 DCHECK([[_otrCardSet tabModel] count] == 0);
620 [_otrCardSet setTabModel:otrModel];
621 }
622
623 - (void)clearInternalState {
624 DCHECK(!_isActive);
625 [[_mainCardSet displayView] removeFromSuperview];
626 [[_otrCardSet displayView] removeFromSuperview];
627
628 // Only deregister from the specific notifications for which this class
629 // registered. Do not use the blanket |removeObserver|, otherwise the low
630 // memory notification is not received and the view is never unloaded.
631 [self deregisterForNotifications];
632
633 _mainCardSet.reset();
634 _otrCardSet.reset();
635 _activeCardSet = nil;
636
637 // Remove gesture recognizers and notifications.
638 [self prepareForDismissal];
639 _gestureStateTracker.reset();
640 _pinchRecognizer.reset();
641 _modeSwitchRecognizer.reset();
642 _swipeGestureRecognizer.reset();
643
644 // The cards need to recompute their sizes the next time they are shown.
645 _initialCardSize.height = _initialCardSize.width = 0.0f;
646 // The scroll view will need to recenter itself relative to its viewport.
647 [_scrollView setContentOffset:CGPointZero];
648 _isBeingDismissed = NO;
649 }
650
651 - (void)viewportSizeWasChanged {
652 [self updateScrollViewContentSize];
653 [_mainCardSet displayViewSizeWasChanged];
654 [_otrCardSet displayViewSizeWasChanged];
655 }
656
657 - (void)updateScrollViewContentSize {
658 // Configure the scroll view to be large enough so that the user could not
659 // scroll to one of its boundaries from the center without also having
660 // reached the corresponding boundary of the stack being scrolled: the
661 // maximum size of the larger of the two stacks plus padding.
662 CGFloat scrollLength = std::max([_mainCardSet maximumStackLength],
663 [_otrCardSet maximumStackLength]);
664 scrollLength += [self scrollLength:[self cardSize]];
665 scrollLength *= 2.0;
666 CGFloat scrollBreadth = [self scrollBreadth:[_scrollView bounds].size];
667 // Changing the scroll view's content size will result in a callback to
668 // |scrollViewDidScroll|.
669 _ignoreScrollCallbacks = YES;
670 [_scrollView setContentSize:[self sizeForScrollLength:scrollLength
671 breadth:scrollBreadth]];
672 _ignoreScrollCallbacks = NO;
673 [self recenterScrollViewIfNecessary];
674 }
675
676 - (void)setUpDisplayViews {
677 CGRect displayViewFrame = CGRectMake(0, 0, [_scrollView frame].size.width,
678 [_scrollView frame].size.height);
679 base::scoped_nsobject<UIView> mainDisplayView(
680 [[UIView alloc] initWithFrame:displayViewFrame]);
681 [mainDisplayView setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
682 UIViewAutoresizingFlexibleHeight];
683 base::scoped_nsobject<UIView> otrDisplayView(
684 [[UIView alloc] initWithFrame:displayViewFrame]);
685 [otrDisplayView setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
686 UIViewAutoresizingFlexibleHeight];
687
688 [_scrollView addSubview:mainDisplayView];
689 [_scrollView addSubview:otrDisplayView];
690 [_mainCardSet setDisplayView:mainDisplayView];
691 [_otrCardSet setDisplayView:otrDisplayView];
692 }
693
694 - (void)prepareForDisplay {
695 [self setUpDisplayViews];
696
697 // Now that the toolbar and the display views are set up, configure the
698 // initial display state.
699 [self displayActiveCardSet];
700
701 _lastInterfaceOrientation = GetInterfaceOrientation();
702 if (_lastInterfaceOrientation == UIInterfaceOrientationUnknown) {
703 CGRect screenBounds = [[UIScreen mainScreen] bounds];
704 _lastInterfaceOrientation =
705 CGRectGetHeight(screenBounds) > CGRectGetWidth(screenBounds)
706 ? UIInterfaceOrientationPortrait
707 : UIInterfaceOrientationLandscapeRight;
708 }
709 [self registerForNotifications];
710
711 // TODO(blundell): Why isn't this recognizer initialized with the
712 // pinch and mode switch recognizers?
713 UIPanGestureRecognizer* panGestureRecognizer =
714 [[UIPanGestureRecognizer alloc] initWithTarget:self
715 action:@selector(handlePanFrom:)];
716 [panGestureRecognizer setMaximumNumberOfTouches:1];
717 _swipeGestureRecognizer.reset(panGestureRecognizer);
718 [[self view] addGestureRecognizer:_swipeGestureRecognizer];
719 [_swipeGestureRecognizer setDelegate:self];
720 }
721
722 - (void)loadView {
723 [super loadView];
724
725 _backgroundView.reset([[UIView alloc] initWithFrame:self.view.bounds]);
726 [_backgroundView setAutoresizingMask:(UIViewAutoresizingFlexibleHeight |
727 UIViewAutoresizingFlexibleWidth)];
728 [self.view addSubview:_backgroundView];
729
730 _toolbarController.reset(
731 [[StackViewToolbarController alloc] initWithStackViewToolbar]);
732 CGRect toolbarFrame = [self.view bounds];
733 toolbarFrame.origin.y = CGRectGetMinY([[_toolbarController view] frame]);
734 toolbarFrame.size.height = CGRectGetHeight([[_toolbarController view] frame]);
735 [[_toolbarController view] setFrame:toolbarFrame];
736 [self.view addSubview:[_toolbarController view]];
737 [self updateToolbarAppearanceWithAnimation:NO];
738
739 InstallBackgroundInView(_backgroundView);
740
741 UIEdgeInsets contentInsets = UIEdgeInsetsMake(
742 toolbarFrame.size.height - kVerticalToolbarOverlap, 0.0, 0.0, 0.0);
743 CGRect scrollViewFrame =
744 UIEdgeInsetsInsetRect(self.view.bounds, contentInsets);
745 _scrollView.reset([[UIScrollView alloc] initWithFrame:scrollViewFrame]);
746 [self.view addSubview:_scrollView];
747 [_scrollView setAutoresizingMask:(UIViewAutoresizingFlexibleHeight |
748 UIViewAutoresizingFlexibleWidth)];
749 [_scrollView setBounces:NO];
750 [_scrollView setScrollsToTop:NO];
751 [_scrollView setClipsToBounds:NO];
752 [_scrollView setShowsVerticalScrollIndicator:NO];
753 [_scrollView setShowsHorizontalScrollIndicator:NO];
754 [_scrollView setDelegate:self];
755
756 _scrollGestureRecognizer = [_scrollView panGestureRecognizer];
757
758 [self prepareForDisplay];
759 }
760
761 - (void)viewWillAppear:(BOOL)animated {
762 _isActive = YES;
763 // Sizing steps need to be done here rather than viewDidLoad since they
764 // depend on the view bounds being correct. Setting initial card size should
765 // be done only once, however, and viewWillAppear: can be called more than
766 // once. For initial display, the transition animation will handle initial
767 // layout. Avoid doing it here since that will potentially cause more views
768 // to be added to the hierarchy synchronously, slowing down inital load. The
769 // rest of the time refreshing is necessary because the card views may have
770 // been purged and recreated or the orientation might have changed while in
771 // a modal view.
772 BOOL isInitialDisplay = _initialCardSize.height == 0.0;
773 if (isInitialDisplay) {
774 [_mainCardSet setObserver:self];
775 [_otrCardSet setObserver:self];
776 [self setInitialCardSizing];
777 [self viewportSizeWasChanged];
778 } else {
779 [self refreshCardDisplayWithAnimation:NO];
780 [self updateToolbarAppearanceWithAnimation:NO];
781 }
782 [self preloadCardViewsAsynchronously];
783
784 // Reset the gesture state tracker to clear gesture-related information from
785 // the last time the stack view was shown.
786 _gestureStateTracker.reset([[GestureStateTracker alloc] init]);
787
788 [super viewWillAppear:animated];
789 }
790
791 - (void)refreshCardDisplayWithAnimation:(BOOL)animates {
792 _lastInterfaceOrientation = GetInterfaceOrientation();
793 [self updateDeckOrientationWithAnimation:animates];
794 [self viewportSizeWasChanged];
795 [_mainCardSet updateCardVisibilities];
796 [_otrCardSet updateCardVisibilities];
797 }
798
799 - (void)viewDidDisappear:(BOOL)animated {
800 if (![self presentedViewController]) {
801 // Stop pre-loading card views if the stack view has been dismissed.
802 [NSObject cancelPreviousPerformRequestsWithTarget:self];
803 _isActive = NO;
804 [self clearInternalState];
805 }
806 [_toolbarController dismissToolsMenuPopup];
807
808 [super viewDidDisappear:animated];
809 }
810
811 - (void)dealloc {
812 [_mainCardSet clearGestureRecognizerTargetAndDelegateFromCards:self];
813 [_otrCardSet clearGestureRecognizerTargetAndDelegateFromCards:self];
814 // Card sets shouldn't have any other references, but nil the observer just
815 // in case one somehow does end up with another ref.
816 [_mainCardSet setObserver:nil];
817 [_otrCardSet setObserver:nil];
818 [self cleanUpViewsAndNotifications];
819 [super dealloc];
820 }
821
822 // Overridden to always return NO, ensuring that the status bar shows in
823 // landscape on iOS8.
824 - (BOOL)prefersStatusBarHidden {
825 return NO;
826 }
827
828 // Called when in the foreground and the OS needs more memory. Release as much
829 // as possible.
830 - (void)didReceiveMemoryWarning {
831 // Releases the view if it doesn't have a superview.
832 [super didReceiveMemoryWarning];
833 _receivedMemoryWarningInSession = YES;
834 [_mainCardSet setKeepOnlyVisibleCardViewsAlive:YES];
835 [_otrCardSet setKeepOnlyVisibleCardViewsAlive:YES];
836
837 if (![self isViewLoaded]) {
838 [self cleanUpViewsAndNotifications];
839 }
840 }
841
842 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orient
843 duration:(NSTimeInterval)duration {
844 [super willRotateToInterfaceOrientation:orient duration:duration];
845 // No animation is performed on rotation if the view is not on screen.
846 if (!_isActive)
847 return;
848
849 // Hide the inactive set. NOTE: Ideally this hiding would be done as a
850 // sliding-off-the-screen animation during the first half of the rotation
851 // animation. However, integrating that custom animation with the default
852 // animation that is being done to the cards on rotation has proved
853 // challenging. For now, the inactive set is invisible during the rotation
854 // itself.
855 [[[self inactiveCardSet] displayView] setHidden:YES];
856 }
857
858 - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)orient
859 duration:(NSTimeInterval)duration {
860 [super willAnimateRotationToInterfaceOrientation:orient duration:duration];
861
862 if (orient == _lastInterfaceOrientation)
863 return;
864
865 // If the stack view controller is not actually active, internal state will
866 // not necessarily be consistent, and the animations could crash as a result.
867 // Short-circuit out in this case (which should happen only in rare race
868 // conditions involving the device being rotated as stack view is
869 // entered/exited).
870 if (!_isActive) {
871 _lastInterfaceOrientation = orient;
872 return;
873 }
874
875 [self updateToolbarAppearanceWithAnimation:YES];
876
877 [_toolbarController dismissToolsMenuPopup];
878
879 [self refreshCardDisplayWithAnimation:YES];
880
881 // Animate the update of the card tabs.
882 CGFloat halfOfTotalDuration = duration / 2.0;
883 void (^cardTabFadeIn)(void) = ^{
884 // Update the card tabs to their new positions instantaneously and then
885 // fade them back in.
886 [self animateActiveSetCardTabsToOpacity:1.0
887 withDuration:halfOfTotalDuration
888 completion:nil];
889 };
890 [self animateActiveSetCardTabsToOpacity:0.0
891 withDuration:halfOfTotalDuration
892 completion:cardTabFadeIn];
893
894 [_gestureStateTracker setResetScrollCardOnNextDrag:YES];
895 [_gestureStateTracker setFirstPinchCardIndex:NSNotFound];
896 [_gestureStateTracker setSecondPinchCardIndex:NSNotFound];
897 }
898
899 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orient {
900 [super didRotateFromInterfaceOrientation:orient];
901 // No animation is performed on rotation if the view is not on screen.
902 if (!_isActive)
903 return;
904
905 // Animate the inactive card set sliding in. NOTE: Ideally this animation
906 // would be done during the second half of the rotation animation. However,
907 // integrating this animation and the default animation that is being done to
908 // the cards on rotation has proved challenging. For now, the inactive set is
909 // invisible during the rotation itself.
910 CardSet* inactiveSet = [self inactiveCardSet];
911 [self updateDeckAxisPositionForCardSet:inactiveSet
912 withShiftAmount:
913 [self shiftOffscreenAmountForCardSet:inactiveSet]];
914 [[inactiveSet displayView] setHidden:NO];
915 [UIView animateWithDuration:kDefaultAnimationDuration
916 delay:0
917 options:0
918 animations:^{
919 [self updateDeckAxisPositionForCardSet:inactiveSet
920 withShiftAmount:0];
921 }
922 completion:nil];
923 }
924
925 - (void)registerForNotifications {
926 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
927 [defaultCenter addObserver:self
928 selector:@selector(allModelTabsHaveClosed:)
929 name:kTabModelAllTabsDidCloseNotification
930 object:nil];
931 }
932
933 - (void)deregisterForNotifications {
934 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
935 [defaultCenter removeObserver:self
936 name:kTabModelAllTabsDidCloseNotification
937 object:nil];
938 }
939
940 - (void)prepareForDismissal {
941 UIView* activeView = [_activeCardSet displayView];
942 [activeView removeGestureRecognizer:_pinchRecognizer];
943 [activeView removeGestureRecognizer:_modeSwitchRecognizer];
944 [activeView removeGestureRecognizer:_swipeDismissesCardRecognizer];
945 [[self view] removeGestureRecognizer:_swipeGestureRecognizer];
946 [_mainCardSet clearGestureRecognizerTargetAndDelegateFromCards:self];
947 [_otrCardSet clearGestureRecognizerTargetAndDelegateFromCards:self];
948 [_scrollView setDelegate:nil];
949 [_scrollView setScrollEnabled:YES];
950 _ignoreScrollCallbacks = NO;
951
952 // Record per-session metrics.
953 UMA_HISTOGRAM_BOOLEAN("MemoryWarning.OccurredDuringCardStackSession",
954 _receivedMemoryWarningInSession);
955 }
956
957 - (void)cleanUpViewsAndNotifications {
958 [_mainCardSet setDisplayView:nil];
959 [_otrCardSet setDisplayView:nil];
960 // Stop pre-loading cards.
961 [NSObject cancelPreviousPerformRequestsWithTarget:self];
962 [_scrollView setDelegate:nil];
963 _scrollView.reset();
964 _backgroundView.reset();
965 [[NSNotificationCenter defaultCenter] removeObserver:self];
966 }
967
968 - (UIStatusBarStyle)preferredStatusBarStyle {
969 // When dismissing the stack view, the status bar's style is updated when this
970 // view controller is still responsible. If the stack view is dismissing into
971 // a non-incognito BVC, the status bar needs to use the default style.
972 BOOL useDefaultStyle = _isBeingDismissed && ![self isCurrentSetIncognito];
973 return useDefaultStyle ? UIStatusBarStyleDefault
974 : UIStatusBarStyleLightContent;
975 }
976
977 #pragma mark -
978 #pragma mark Card and Stack Construction
979
980 - (void)preloadCardViewsAsynchronously {
981 // Start the deferred loading of card views. Defers the pre-loading slightly
982 // in order to give the initially visible cards a head start.
983 [NSObject cancelPreviousPerformRequestsWithTarget:self];
984 [self performSelector:@selector(preloadNextCardView)
985 withObject:nil
986 afterDelay:0.01];
987 }
988
989 - (void)preloadNextCardView {
990 if (_isBeingDismissed)
991 return;
992 // Preload one card from the active set, or if that's already loaded, from
993 // the other set.
994 BOOL preloadedCard = [_activeCardSet preloadNextCard];
995 if (!preloadedCard)
996 preloadedCard = [[self inactiveCardSet] preloadNextCard];
997 // If there was a card to preload, queue the next round.
998 if (preloadedCard) {
999 [self performSelector:@selector(preloadNextCardView)
1000 withObject:nil
1001 afterDelay:0];
1002 } else {
1003 [_testDelegate stackViewControllerPreloadCardViewsDidEnd];
1004 }
1005 }
1006
1007 - (void)animateOutCardView:(CardView*)cardView
1008 delay:(NSTimeInterval)delay
1009 clockwise:(BOOL)clockwise
1010 completion:(ProceduralBlock)completion {
1011 DCHECK(cardView);
1012 void (^toDoWhenDone)(void) = ^{
1013 [cardView removeFromSuperview];
1014 if (completion)
1015 completion();
1016 };
1017 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
1018 ios_internal::page_animation_util::AnimateOutWithCompletion(
1019 cardView, delay, clockwise, isPortrait, toDoWhenDone);
1020 }
1021
1022 - (void)removeAllCardsFromSet:(CardSet*)cardSet {
1023 // Ignore model updates while the cards are closing, to batch all the
1024 // re-laying-out work.
1025 [cardSet setIgnoresTabModelChanges:YES];
1026
1027 NSTimeInterval delay = 0;
1028 NSArray* cards = [cardSet cards];
1029 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
1030
1031 // Find the last visible card.
1032 StackCard* lastVisibleCard = nil;
1033 for (StackCard* card in [cards reverseObjectEnumerator]) {
1034 if ([card viewIsLive]) {
1035 lastVisibleCard = card;
1036 break;
1037 }
1038 }
1039 if (lastVisibleCard == nil) {
1040 [cardSet.tabModel closeAllTabs];
1041 return;
1042 }
1043
1044 for (StackCard* card in cards) {
1045 NSInteger cardIndex = [cards indexOfObject:card];
1046 DCHECK(cardIndex != NSNotFound);
1047 BOOL cardWasCollapsed = [cardSet cardIsCollapsed:card];
1048
1049 if ([card viewIsLive]) {
1050 void (^toDoWhenDone)(void) = NULL;
1051 if (card == lastVisibleCard) {
1052 toDoWhenDone = ^{
1053 [cardSet.tabModel closeAllTabs];
1054 };
1055 }
1056 [self animateOutCardView:card.view
1057 delay:delay
1058 clockwise:isPortrait
1059 completion:toDoWhenDone];
1060 } else {
1061 // It's too late to create a view for this card now. This case should only
1062 // occur if the card was covered, meaning that its animation out would
1063 // have been invisible anyway.
1064 DCHECK(cardWasCollapsed);
1065 }
1066
1067 // Add a delay before the next card's animation if this card was not
1068 // collapsed into the next card.
1069 if (!cardWasCollapsed)
1070 delay += kCascadingCardCloseDelay;
1071 }
1072 }
1073
1074 - (void)disableGestureHandlers {
1075 // Disable gesture handlers before modifying the stack. Don't call this too
1076 // late or a gesture callback could occur while still in the old state of the
1077 // world.
1078 // (see the comment in -cardSet:willRemoveCard:atIndex for details).
1079 [_scrollView setScrollEnabled:NO];
1080 _pinchRecognizer.get().enabled = NO;
1081 _swipeGestureRecognizer.get().enabled = NO;
1082 }
1083
1084 - (void)enableGestureHandlers {
1085 // Reenable gesture handlers after modifying the stack. Don't call this too
1086 // early or a gesture callback could occur while still in the old state of the
1087 // world.
1088 // (see the comment in -cardSet:willRemoveCard:atIndex for details).
1089 [_scrollView setScrollEnabled:YES];
1090 _pinchRecognizer.get().enabled = YES;
1091 _swipeGestureRecognizer.get().enabled = YES;
1092 }
1093
1094 - (void)activeCardCountChanged {
1095 // Cancel any outstanding gestures (see the comment in
1096 // -cardSet:willRemoveCard:atIndex).
1097 [self disableGestureHandlers];
1098 [self enableGestureHandlers];
1099 }
1100
1101 - (void)setInitialCardSizing {
1102 DCHECK(_initialCardSize.height == 0.0);
1103 CGFloat viewportBreadth = [self scrollBreadth:[_scrollView bounds].size];
1104 _initialCardSize = [self cardSizeForBreadth:viewportBreadth];
1105
1106 // Configure the stack layout behaviors. This is done only once because the
1107 // fan-out, margins, etc. should stay the same even if the cards change size
1108 // due to rotation.
1109 [self updateDeckOrientationWithAnimation:NO];
1110 [_mainCardSet configureLayoutParametersWithMargin:
1111 ios_internal::page_animation_util::kCardMargin];
1112 [_otrCardSet configureLayoutParametersWithMargin:
1113 ios_internal::page_animation_util::kCardMargin];
1114 }
1115
1116 - (void)updateDeckOrientationWithAnimation:(BOOL)animates {
1117 [self updateDeckAxisPositions];
1118 [self updateCardSizesWithAnimation:animates];
1119 }
1120
1121 - (void)updateCardSizesWithAnimation:(BOOL)animates {
1122 CGSize cardSize = [self cardSize];
1123 NSTimeInterval animationDuration = animates ? kDefaultAnimationDuration : 0;
1124 [UIView animateWithDuration:animationDuration
1125 delay:0
1126 options:UIViewAnimationOptionBeginFromCurrentState
1127 animations:^{
1128 [_mainCardSet setCardSize:cardSize];
1129 [_otrCardSet setCardSize:cardSize];
1130 }
1131 completion:nil];
1132 }
1133
1134 - (void)animateActiveSetCardTabsToOpacity:(CGFloat)opacity
1135 withDuration:(CGFloat)duration
1136 completion:(ProceduralBlock)completion {
1137 [UIView animateWithDuration:duration
1138 delay:0
1139 options:(UIViewAnimationOptionBeginFromCurrentState |
1140 UIViewAnimationOptionOverrideInheritedDuration)
1141 animations:^{
1142 for (StackCard* card in [_activeCardSet cards]) {
1143 if (card.viewIsLive)
1144 [card.view setTabOpacity:opacity];
1145 }
1146 }
1147 completion:^(BOOL) {
1148 if (completion)
1149 completion();
1150 }];
1151 }
1152
1153 - (void)updateDeckAxisPositions {
1154 [self updateDeckAxisPositionsWithShiftAmount:0];
1155 }
1156
1157 - (void)updateDeckAxisPositionsWithShiftAmount:(CGFloat)shiftAmount {
1158 [self updateDeckAxisPositionForCardSet:_activeCardSet
1159 withShiftAmount:shiftAmount];
1160 [self updateDeckAxisPositionForCardSet:[self inactiveCardSet]
1161 withShiftAmount:shiftAmount];
1162 }
1163
1164 - (void)updateDeckAxisPositionForCardSet:(CardSet*)cardSet
1165 withShiftAmount:(CGFloat)shiftAmount {
1166 // Skip axis layout if card size hasn't been set up yet; it will be handled
1167 // when card size is.
1168 if (_initialCardSize.height == 0.0)
1169 return;
1170
1171 if (!cardSet)
1172 return;
1173
1174 CGFloat viewportBreadth =
1175 [self scrollBreadth:[_mainCardSet displayView].bounds.size];
1176 CGFloat fullDisplayBreadth =
1177 [self bothDecksShouldBeDisplayed]
1178 ? (viewportBreadth * kActiveDeckDisplayFraction)
1179 : viewportBreadth;
1180
1181 CGFloat center = (fullDisplayBreadth / 2.0);
1182 if ([self isCurrentSetIncognito])
1183 center += viewportBreadth - fullDisplayBreadth;
1184 // Adjust the set's center if it's not the active card set.
1185 if (cardSet != _activeCardSet) {
1186 CGFloat inactiveSetDelta = fullDisplayBreadth -
1187 ios_internal::page_animation_util::kCardMargin +
1188 kCardFrameInset;
1189 center = [self isCurrentSetIncognito] ? center - inactiveSetDelta
1190 : center + inactiveSetDelta;
1191 }
1192 center += shiftAmount;
1193
1194 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
1195 [cardSet setLayoutAxisPosition:center isVertical:isPortrait];
1196 }
1197
1198 - (CGFloat)shiftOffscreenAmountForCardSet:(CardSet*)cardSet {
1199 if (!cardSet)
1200 return 0;
1201
1202 CGFloat viewportBreadth =
1203 [self scrollBreadth:[_activeCardSet displayView].bounds.size];
1204 // The incognito card set moves offscreen to the right; the main card set
1205 // moves offscreen to the left.
1206 CGFloat offset = (1 - kActiveDeckDisplayFraction) * viewportBreadth;
1207 if (cardSet == _mainCardSet)
1208 offset = -offset;
1209 return offset;
1210 }
1211
1212 - (BOOL)bothDecksShouldBeDisplayed {
1213 return [[_otrCardSet cards] count] > 0;
1214 }
1215
1216 #pragma mark -
1217 #pragma mark Current Set Handling
1218
1219 - (BOOL)isCurrentSetIncognito {
1220 return _activeCardSet == _otrCardSet.get();
1221 }
1222
1223 - (CardSet*)inactiveCardSet {
1224 return [self isCurrentSetIncognito] ? _mainCardSet.get() : _otrCardSet.get();
1225 }
1226
1227 - (void)setActiveCardSet:(CardSet*)cardSet {
1228 DCHECK(cardSet);
1229 if (cardSet == _activeCardSet)
1230 return;
1231 [self activeCardCountChanged];
1232 _activeCardSet = cardSet;
1233
1234 [self displayActiveCardSet];
1235 }
1236
1237 - (void)displayActiveCardSet {
1238 UIView* activeView = [_activeCardSet displayView];
1239 DCHECK(activeView);
1240 UIView* inactiveView = [[self inactiveCardSet] displayView];
1241
1242 [_scrollView bringSubviewToFront:activeView];
1243
1244 // |_swipeGestureRecognizer| is added to the main SVC view, so don't add or
1245 // remove that here.
1246 // TODO(blundell): Figure out which recognizers need to be associated with the
1247 // active display view and which can just be on the superview.
1248 [inactiveView removeGestureRecognizer:_pinchRecognizer];
1249 [inactiveView removeGestureRecognizer:_modeSwitchRecognizer];
1250 [inactiveView removeGestureRecognizer:_swipeDismissesCardRecognizer];
1251 [activeView addGestureRecognizer:_pinchRecognizer];
1252 [activeView addGestureRecognizer:_modeSwitchRecognizer];
1253 [activeView addGestureRecognizer:_swipeDismissesCardRecognizer];
1254
1255 activeView.accessibilityElementsHidden = NO;
1256 inactiveView.accessibilityElementsHidden = YES;
1257
1258 _inActiveDeckChangeAnimation = YES; // This flag is used for testing.
1259 [UIView animateWithDuration:kDefaultAnimationDuration
1260 delay:0
1261 options:UIViewAnimationOptionBeginFromCurrentState
1262 animations:^{
1263 [self updateDeckAxisPositions];
1264 }
1265 completion:^(BOOL finished) {
1266 _inActiveDeckChangeAnimation = NO;
1267 }];
1268
1269 [self updateToolbarAppearanceWithAnimation:YES];
1270 }
1271
1272 - (void)displayMainCardSetOnly {
1273 [self updateCardSizesWithAnimation:YES];
1274 if ([self isCurrentSetIncognito]) {
1275 [self setActiveCardSet:[self inactiveCardSet]];
1276 } else {
1277 // Ensure that layout axis position is up to date.
1278 [self displayActiveCardSet];
1279 }
1280 }
1281
1282 - (void)updateToolbarAppearanceWithAnimation:(BOOL)animate {
1283 [_toolbarController setTabCount:[_activeCardSet.cards count]];
1284 [[_toolbarController openNewTabButton]
1285 setIncognito:[self isCurrentSetIncognito]
1286 animated:animate];
1287
1288 // Position the toolbar above/below the cards depending on the current state
1289 // of the cards and the device. In landscape with multiple stacks, the cards
1290 // must be behind the toolbar to avoid covering it. In all other cases, the
1291 // cards are positioned to go in front of the toolbar.
1292 BOOL toolbarShouldHaveBackground = NO;
1293 if (([[_otrCardSet cards] count] > 0) && IsLandscape())
1294 toolbarShouldHaveBackground = YES;
1295
1296 NSUInteger scrollViewIndex =
1297 [[[self view] subviews] indexOfObject:_scrollView];
1298 NSUInteger toolbarViewIndex =
1299 [[[self view] subviews] indexOfObject:[_toolbarController view]];
1300 BOOL toolbarInFrontOfScrollView = (toolbarViewIndex > scrollViewIndex);
1301
1302 // If moving the toolbar to the front, have it cover the cards before any
1303 // animation of the background starts to occur, as this looks cleanest.
1304 if (toolbarShouldHaveBackground && !toolbarInFrontOfScrollView) {
1305 [[self view] exchangeSubviewAtIndex:scrollViewIndex
1306 withSubviewAtIndex:toolbarViewIndex];
1307 }
1308
1309 void (^updateToolbar)(void) = ^{
1310 CGFloat alpha = toolbarShouldHaveBackground ? 1.0 : 0.0;
1311 [_toolbarController backgroundView].alpha = alpha;
1312 [_toolbarController shadowView].alpha = alpha;
1313 };
1314
1315 // If moving the toolbar to the back, have the cards move forward only after
1316 // the toolbar background finishes disappearing, as this looks cleanest.
1317 void (^toDoWhenDone)(void) = ^{
1318 if (!toolbarShouldHaveBackground && toolbarInFrontOfScrollView) {
1319 [[self view] exchangeSubviewAtIndex:scrollViewIndex
1320 withSubviewAtIndex:toolbarViewIndex];
1321 [_scrollView setClipsToBounds:NO];
1322 }
1323 };
1324
1325 if (animate) {
1326 [UIView animateWithDuration:kDefaultAnimationDuration
1327 delay:0
1328 options:UIViewAnimationOptionBeginFromCurrentState
1329 animations:^{
1330 updateToolbar();
1331 }
1332 completion:^(BOOL finished) {
1333 toDoWhenDone();
1334 }];
1335 } else {
1336 updateToolbar();
1337 toDoWhenDone();
1338 }
1339 }
1340
1341 #pragma mark -
1342 #pragma mark Sizing/Measuring Helpers
1343
1344 - (CGSize)cardSize {
1345 DCHECK(_initialCardSize.height != 0.0);
1346 CGFloat availableBreadth = [self scrollBreadth:[_scrollView bounds].size];
1347 if ([self bothDecksShouldBeDisplayed])
1348 availableBreadth *= kActiveDeckDisplayFraction;
1349 CGSize idealCardSize = [self cardSizeForBreadth:availableBreadth];
1350
1351 // Crop the ideal size so that it's no bigger than the initial size.
1352 return CGSizeMake(std::min(idealCardSize.width, _initialCardSize.width),
1353 std::min(idealCardSize.height, _initialCardSize.height));
1354 }
1355
1356 - (CGSize)cardSizeForBreadth:(CGFloat)breadth {
1357 BOOL isPortrait = IsPortrait();
1358 CGFloat cardBreadth =
1359 breadth - 2 * ios_internal::page_animation_util::kCardMargin;
1360 CGFloat contentBreadthInset =
1361 isPortrait ? kCardImageInsets.left + kCardImageInsets.right
1362 : kCardImageInsets.top + kCardImageInsets.bottom;
1363 CGFloat contentBreadth = cardBreadth - contentBreadthInset;
1364 CGSize viewSize = [_scrollView bounds].size;
1365 CGFloat aspectRatio =
1366 [self scrollLength:viewSize] / [self scrollBreadth:viewSize];
1367 CGFloat contentLength = std::floor(aspectRatio * contentBreadth);
1368 CGFloat contentLengthInset =
1369 isPortrait ? kCardImageInsets.top + kCardImageInsets.bottom
1370 : kCardImageInsets.left + kCardImageInsets.right;
1371 CGFloat cardLength = contentLength + contentLengthInset;
1372 // Truncate the card length so that the entire card can be visible at once.
1373 CGFloat viewLength = isPortrait ? viewSize.height : viewSize.width;
1374 CGFloat truncatedCardLength = viewLength -
1375 ios_internal::page_animation_util::kCardMargin -
1376 kCardBottomPadding;
1377 cardLength = std::min(cardLength, truncatedCardLength);
1378 return [self sizeForScrollLength:cardLength breadth:cardBreadth];
1379 }
1380
1381 - (CGFloat)scrollOffsetAmountForPoint:(CGPoint)point {
1382 return IsPortrait() ? point.y : point.x;
1383 }
1384
1385 - (CGFloat)scrollOffsetAmountForPosition:(LayoutRectPosition)position {
1386 return IsPortrait() ? position.originY : position.leading;
1387 }
1388
1389 - (CGPoint)scrollOffsetPointWithAmount:(CGFloat)offset {
1390 return IsPortrait() ? CGPointMake(0, offset) : CGPointMake(offset, 0);
1391 }
1392
1393 - (CGFloat)scrollLength:(CGSize)size {
1394 return IsPortrait() ? size.height : size.width;
1395 }
1396
1397 - (CGFloat)scrollBreadth:(CGSize)size {
1398 return IsPortrait() ? size.width : size.height;
1399 }
1400
1401 - (CGSize)sizeForScrollLength:(CGFloat)length breadth:(CGFloat)breadth {
1402 return IsPortrait() ? CGSizeMake(breadth, length)
1403 : CGSizeMake(length, breadth);
1404 }
1405
1406 - (CGRect)inactiveDeckRegion {
1407 // If only one deck is showing, there's no inactive deck region.
1408 if (![self bothDecksShouldBeDisplayed])
1409 return CGRectZero;
1410
1411 CGSize viewportSize = [_activeCardSet displayView].frame.size;
1412 CGFloat viewportBreadth = [self scrollBreadth:viewportSize];
1413 CGFloat inactiveBreadth = (1 - kActiveDeckDisplayFraction) * viewportBreadth;
1414 CGSize regionSize = [self sizeForScrollLength:[self scrollLength:viewportSize]
1415 breadth:inactiveBreadth];
1416 CGPoint regionOrigin = [_scrollView contentOffset];
1417 if (IsPortrait()) {
1418 BOOL inactiveOnRight = UseRTLLayout() == [self isCurrentSetIncognito];
1419 if (inactiveOnRight)
1420 regionOrigin.x = viewportBreadth - regionSize.width;
1421 } else {
1422 BOOL inactiveOnBottom = ![self isCurrentSetIncognito];
1423 if (inactiveOnBottom)
1424 regionOrigin.y = viewportBreadth - regionSize.height;
1425 }
1426 return {regionOrigin, regionSize};
1427 }
1428
1429 - (NSUInteger)indexOfCardAtPoint:(CGPoint)point {
1430 UIView* view = [_activeCardSet.displayView hitTest:point withEvent:nil];
1431 while (view && ![view isKindOfClass:[CardView class]]) {
1432 view = [view superview];
1433 }
1434 if (!view)
1435 return NSNotFound;
1436 StackCard* card = [self cardForView:(CardView*)view];
1437 return [_activeCardSet.cards indexOfObject:card];
1438 }
1439
1440 #pragma mark -
1441 #pragma mark Stack View Transition Helpers
1442
1443 // Determine what should be the first visible card. Preference is to start one
1444 // card before the current card so that the current card ends up in the middle
1445 // of the visible cards. However, if the current card is the last in a stack of
1446 // > 2 cards, start two cards before so that the screen is fully populated (and
1447 // if the current card is the first card, the only option is to start with the
1448 // first card).
1449 - (NSUInteger)startIndexOfInitialFanoutForCardSet:(CardSet*)cardSet {
1450 if ([[cardSet cards] count] == 0)
1451 return 0;
1452 NSUInteger currentCardIndex =
1453 [cardSet.tabModel indexOfTab:cardSet.tabModel.currentTab];
1454 NSUInteger startingCardIndex =
1455 (currentCardIndex == 0) ? 0 : currentCardIndex - 1;
1456 if ((currentCardIndex > 1) &&
1457 (currentCardIndex == ([cardSet.tabModel count] - 1)))
1458 startingCardIndex -= 1;
1459 return startingCardIndex;
1460 }
1461
1462 - (void)showWithSelectedTabAnimation {
1463 [self animateTransitionWithStyle:STACK_TRANSITION_STYLE_PRESENTING];
1464
1465 [_testDelegate stackViewControllerShowWithSelectedTabAnimationDidStart];
1466
1467 [_activeCardSet.currentCard setIsActiveTab:YES];
1468
1469 // When in accessbility mode, fan out cards from the start, announce open tabs
1470 // and move the VoiceOver cursor to the New Tab button. Fanning out the cards
1471 // from the start eliminates the screen change that would otherwise occur when
1472 // moving the VoiceOver cursor from the Show Tabs button to the card stack.
1473 if (UIAccessibilityIsVoiceOverRunning()) {
1474 [_activeCardSet fanOutCardsWithStartIndex:0];
1475 [self postOpenTabsAccessibilityNotification];
1476 UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
1477 _toolbarController.get().view);
1478 }
1479 }
1480
1481 - (void)cancelTransitionAnimation {
1482 // Set up transaction.
1483 [CATransaction begin];
1484 [CATransaction setCompletionBlock:^{
1485 [self finishTransitionAnimation];
1486 }];
1487 self.transitionWasCancelled = YES;
1488 // Reverse all the animations.
1489 [self reverseDummyToolbarBackgroundViewAnimation];
1490 [self reverseTransitionAnimationsForCardSet:_activeCardSet];
1491 [self reverseTransitionAnimationsForCardSet:[self inactiveCardSet]];
1492 [self.transitionToolbarController reverseTransitionAnimations];
1493 // Commit the transaction. Since the animations added for the previous
1494 // transition are all removed, this commit will call the previous
1495 // animation's completion block.
1496 [CATransaction commit];
1497 }
1498
1499 - (void)finishTransitionAnimation {
1500 // Early return if cancelled.
1501 if (self.transitionWasCancelled) {
1502 // Notify the delegates.
1503 [self notifyDelegatesTransitionFinished];
1504 // When transitions are cancelled, reverse the transition style so that the
1505 // new completion block sends the correct delegate methods.
1506 self.transitionStyle =
1507 self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING
1508 ? STACK_TRANSITION_STYLE_DISMISSING
1509 : STACK_TRANSITION_STYLE_PRESENTING;
1510 self.transitionWasCancelled = NO;
1511 return;
1512 }
1513 // Clean up card view animations.
1514 for (StackCard* card in _activeCardSet.cards) {
1515 if ([card viewIsLive])
1516 [card.view cleanUpAnimations];
1517 }
1518 for (StackCard* card in [self inactiveCardSet].cards) {
1519 if ([card viewIsLive])
1520 [card.view cleanUpAnimations];
1521 }
1522 // Clean up toolbar animations.
1523 [self.transitionToolbarController.view removeFromSuperview];
1524 [self.transitionToolbarOwner reparentToolbarController];
1525 [self.transitionToolbarController cleanUpTransitionAnimations];
1526 self.transitionToolbarController.view.animatingTransition = NO;
1527 [self.transitionToolbarController view].frame = self.transitionToolbarFrame;
1528 self.transitionToolbarController = nil;
1529 self.transitionToolbarFrame = CGRectZero;
1530 self.transitionToolbarOwner = nil;
1531 // Clean up dummy toolbar background.
1532 [self.dummyToolbarBackgroundView removeFromSuperview];
1533 self.dummyToolbarBackgroundView = nil;
1534 // Notify the delegates.
1535 [self notifyDelegatesTransitionFinished];
1536 // Reset the current transition style.
1537 StackTransitionStyle transitionStyleAtFinish = self.transitionStyle;
1538 self.transitionStyle = STACK_TRANSITION_STYLE_NONE;
1539 // Restore the original subview ordering.
1540 [self reorderSubviewsForTransition];
1541 // Dismiss immediately if a card was selected mid-presentation.
1542 if (self.transitionTappedCard) {
1543 _activeCardSet.currentCard = self.transitionTappedCard;
1544 self.transitionTappedCard = nil;
1545 [self dismissWithSelectedTabAnimation];
1546 }
1547
1548 if (transitionStyleAtFinish == STACK_TRANSITION_STYLE_DISMISSING) {
1549 // Dismissal is complete and delegate was told that stack view has been
1550 // dismissed. Make sure that internal state reflects that.
1551 _isActive = NO;
1552 [self clearInternalState];
1553 }
1554 }
1555
1556 - (void)notifyDelegatesTransitionFinished {
1557 // Notify delegates.
1558 DCHECK_NE(self.transitionStyle, STACK_TRANSITION_STYLE_NONE);
1559 if (self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING) {
1560 [_testDelegate stackViewControllerShowWithSelectedTabAnimationDidEnd];
1561 [_delegate tabSwitcherPresentationTransitionDidEnd:self];
1562 } else {
1563 [_delegate tabSwitcherDismissTransitionDidEnd:self];
1564 }
1565 }
1566
1567 - (void)animateTransitionWithStyle:(StackTransitionStyle)transitionStyle {
1568 // If the dummy toolbar background view is instantiated, reverse the current
1569 // transition animations.
1570 if (self.dummyToolbarBackgroundView) {
1571 [self cancelTransitionAnimation];
1572 return;
1573 }
1574
1575 // The transition style must be specified.
1576 DCHECK_NE(transitionStyle, STACK_TRANSITION_STYLE_NONE);
1577 self.transitionStyle = transitionStyle;
1578 BOOL isPresenting = self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING;
1579
1580 // Get reference to toolbar for transition.
1581 self.transitionToolbarOwner = [_delegate tabSwitcherTransitionToolbarOwner];
1582 self.transitionToolbarController =
1583 [self.transitionToolbarOwner relinquishedToolbarController];
1584 self.transitionToolbarController.view.animatingTransition = YES;
1585 self.transitionToolbarFrame = self.transitionToolbarController.view.frame;
1586
1587 // Create dummy toolbar background view.
1588 self.dummyToolbarBackgroundView =
1589 [[[UIView alloc] initWithFrame:CGRectZero] autorelease];
1590 [self.dummyToolbarBackgroundView setClipsToBounds:YES];
1591
1592 // Set the transition completion block.
1593 [CATransaction begin];
1594 [CATransaction setCompletionBlock:^{
1595 [self finishTransitionAnimation];
1596 }];
1597
1598 // Slide in/out the inactive card set.
1599 [self animateInactiveSetTransition];
1600
1601 // The current card's frame is necessary for the toolbar animation below. For
1602 // dismissals, the toolbar animates from the card's current frame (i.e. the
1603 // frame before the animation is added). For presentation, the toolbar
1604 // animates to the final frame (i.e. the frame after the animation is added).
1605 LayoutRect currentCardLayout = _activeCardSet.currentCard.layout;
1606 [self animateActiveSetTransition];
1607 if (isPresenting)
1608 currentCardLayout = _activeCardSet.currentCard.layout;
1609 CGRect currentCardFrame =
1610 AlignRectOriginAndSizeToPixels(LayoutRectGetRect(currentCardLayout));
1611
1612 // Animate the dummy toolbar background view.
1613 [self animateDummyToolbarForCardFrame:currentCardFrame
1614 transitionStyle:transitionStyle];
1615
1616 // Animate the transition toolbar.
1617 [self animateTransitionToolbarWithCardFrame:currentCardFrame
1618 transitionStyle:transitionStyle];
1619
1620 // Update the order of the view hierarchy.
1621 [self reorderSubviewsForTransition];
1622
1623 [CATransaction commit];
1624 }
1625
1626 - (void)reorderSubviewsForTransition {
1627 if (self.transitionStyle != STACK_TRANSITION_STYLE_NONE) {
1628 // Add the card set display views to the main view and insert the toolbar
1629 // between them.
1630 [self.view addSubview:[self inactiveCardSet].displayView];
1631 [self inactiveCardSet].displayView.frame = [_scrollView frame];
1632 [self.view addSubview:_activeCardSet.displayView];
1633 _activeCardSet.displayView.frame = [_scrollView frame];
1634 [self.view insertSubview:[_toolbarController view]
1635 belowSubview:_activeCardSet.displayView];
1636 } else {
1637 // Add the display views back into the scroll view.
1638 [_scrollView addSubview:[self inactiveCardSet].displayView];
1639 [_scrollView addSubview:_activeCardSet.displayView];
1640 [self updateToolbarAppearanceWithAnimation:NO];
1641 [self alignDisplayViewsToViewport];
1642 }
1643 }
1644
1645 - (void)animateCardSet:(CardSet*)cardSet
1646 fromBeginLayouts:(std::vector<LayoutRect>)beginLayouts
1647 toEndLayouts:(std::vector<LayoutRect>)endLayouts {
1648 NSUInteger cardCount = [cardSet.cards count];
1649 DCHECK_EQ(cardCount, beginLayouts.size());
1650 DCHECK_EQ(cardCount, endLayouts.size());
1651
1652 [CATransaction begin];
1653 [CATransaction setDisableActions:YES];
1654 // Place cards into final position.
1655 for (NSUInteger i = 0; i < cardCount; ++i)
1656 [cardSet.cards[i] setLayout:endLayouts[i]];
1657 // For presentation, update visibilty so only cards that will ultimately be
1658 // shown are live.
1659 BOOL isPresenting = self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING;
1660 if (isPresenting)
1661 [cardSet updateCardVisibilities];
1662 [CATransaction commit];
1663
1664 // Animate each card to its final frame.
1665 StackCard* currentCard = cardSet.currentCard;
1666 BOOL isActiveCardSet = (cardSet == _activeCardSet);
1667 for (NSUInteger i = 0; i < cardCount; ++i) {
1668 StackCard* card = cardSet.cards[i];
1669 if ([card viewIsLive]) {
1670 CardTabAnimationStyle tabAnimationStyle = CARD_TAB_ANIMATION_STYLE_NONE;
1671 if (isActiveCardSet && card == currentCard) {
1672 tabAnimationStyle = isPresenting ? CARD_TAB_ANIMATION_STYLE_FADE_IN
1673 : CARD_TAB_ANIMATION_STYLE_FADE_OUT;
1674 }
1675 [card.view animateFromBeginFrame:LayoutRectGetRect(beginLayouts[i])
1676 toEndFrame:LayoutRectGetRect(endLayouts[i])
1677 tabAnimationStyle:tabAnimationStyle];
1678 }
1679 }
1680 }
1681
1682 - (void)reverseTransitionAnimationsForCardSet:(CardSet*)cardSet {
1683 for (StackCard* card in cardSet.cards) {
1684 if ([card viewIsLive])
1685 [card.view reverseAnimations];
1686 }
1687 }
1688
1689 - (void)animateActiveSetTransition {
1690 // Early return for an empty active card set.
1691 if (![_activeCardSet.cards count])
1692 return;
1693
1694 std::vector<LayoutRect> beginLayouts;
1695 std::vector<LayoutRect> endLayouts;
1696 BOOL isPresenting = self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING;
1697 if (isPresenting) {
1698 // For presentation, animate from transition frames to fan frames.
1699 NSUInteger activeSetStartIndex =
1700 [self startIndexOfInitialFanoutForCardSet:_activeCardSet];
1701 beginLayouts = [self cardTransitionLayouts];
1702 [_activeCardSet fanOutCardsWithStartIndex:activeSetStartIndex];
1703 endLayouts = [_activeCardSet cardLayouts];
1704 } else {
1705 // For dismissal, animate from the cards' current frames to the transition
1706 // frames.
1707 beginLayouts = [_activeCardSet cardLayouts];
1708 endLayouts = [self cardTransitionLayouts];
1709 // For dismissals, the status bar needs to be updated early.
1710 [self performSelector:@selector(setNeedsStatusBarAppearanceUpdate)
1711 withObject:nil
1712 afterDelay:kDismissalStatusBarUpdateDelay];
1713 // Ensure that the current card view is visible.
1714 _activeCardSet.currentCard.view.hidden = NO;
1715 }
1716
1717 // Add animations.
1718 [self animateCardSet:_activeCardSet
1719 fromBeginLayouts:beginLayouts
1720 toEndLayouts:endLayouts];
1721 }
1722
1723 - (void)animateInactiveSetTransition {
1724 // Early return for an emtpy inactive card set.
1725 CardSet* inactiveCardSet = [self inactiveCardSet];
1726 if (![[inactiveCardSet cards] count])
1727 return;
1728
1729 BOOL isPresenting = self.transitionStyle == STACK_TRANSITION_STYLE_PRESENTING;
1730
1731 // Calculate transition animation card frames
1732 if (isPresenting) {
1733 // For presentation, fan out the cards for the transition. Otherwise, use
1734 // the current frames of the cards.
1735 NSUInteger inactiveSetStartIndex =
1736 [self startIndexOfInitialFanoutForCardSet:inactiveCardSet];
1737 [inactiveCardSet fanOutCardsWithStartIndex:inactiveSetStartIndex];
1738 }
1739 std::vector<LayoutRect> cardStackLayouts = [inactiveCardSet cardLayouts];
1740 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
1741 CGFloat shiftAmount = [self shiftOffscreenAmountForCardSet:inactiveCardSet];
1742 std::vector<LayoutRect> shiftedStackLayouts;
1743 for (const auto& cardLayout : cardStackLayouts) {
1744 LayoutRect shiftedLayout = cardLayout;
1745 if (isPortrait)
1746 shiftedLayout.position.leading += shiftAmount;
1747 else
1748 shiftedLayout.position.originY += shiftAmount;
1749 shiftedStackLayouts.push_back(shiftedLayout);
1750 }
1751
1752 std::vector<LayoutRect> beginLayouts =
1753 isPresenting ? shiftedStackLayouts : cardStackLayouts;
1754 std::vector<LayoutRect> endLayouts =
1755 isPresenting ? cardStackLayouts : shiftedStackLayouts;
1756
1757 // Add animations.
1758 [self animateCardSet:inactiveCardSet
1759 fromBeginLayouts:beginLayouts
1760 toEndLayouts:endLayouts];
1761 }
1762
1763 - (void)animateDummyToolbarForCardFrame:(CGRect)cardFrame
1764 transitionStyle:(StackTransitionStyle)transitionStyle {
1765 // Install the dummy toolbar background view into the card tab.
1766 CardView* cardView = _activeCardSet.currentCard.view;
1767 [cardView installDummyToolbarBackgroundView:self.dummyToolbarBackgroundView];
1768
1769 // When calculating the frames below, convert them into the card's tab's
1770 // coordinate system, whose origin is at |cardTabOriginOffset| from the card's
1771 // frame origin.
1772 CGVector cardTabOriginOffset =
1773 CGVectorMake(kCardFrameInset, kCardTabTopInset);
1774
1775 // The card's frame image extends beyond the edges of the screen when the
1776 // current card is scaled to the full content area, so extend the toolbar
1777 // background to match the card's width. For the NTP toolbar, extend the
1778 // frame downward so that it covers the portion of the card frame that
1779 // overlaps with the content snapshot.
1780 UIView* toolbarView = [_toolbarController view];
1781 UIView* displayView = _activeCardSet.displayView;
1782 CGRect screenToolbarFrame = [displayView convertRect:toolbarView.frame
1783 fromView:toolbarView.superview];
1784 CGFloat bottomOutset = [self.transitionToolbarController
1785 isKindOfClass:[NewTabPageToolbarController class]]
1786 ? -kCardFrameImageSnapshotOverlap
1787 : 0.0;
1788 UIEdgeInsets screenToolbarFrameOutsets =
1789 UIEdgeInsetsMake(0.0, kCardFrameInset - kCardImageInsets.left,
1790 bottomOutset, kCardFrameInset - kCardImageInsets.right);
1791 screenToolbarFrame =
1792 UIEdgeInsetsInsetRect(screenToolbarFrame, screenToolbarFrameOutsets);
1793 CGPoint screenCardOrigin =
1794 CGPointMake(displayView.bounds.origin.x - kCardImageInsets.left,
1795 displayView.bounds.origin.y - kCardImageInsets.top);
1796 screenToolbarFrame = CGRectOffset(
1797 screenToolbarFrame, -(screenCardOrigin.x + cardTabOriginOffset.dx),
1798 -(screenCardOrigin.y + cardTabOriginOffset.dy));
1799
1800 // The frame should interpolate to the frame of the card's tab view.
1801 CGRect cardToolbarFrame =
1802 CGRectInset(cardFrame, kCardFrameInset, kCardFrameInset);
1803 cardToolbarFrame.size.height = kCardImageInsets.top - kCardFrameInset;
1804 cardToolbarFrame = CGRectOffset(
1805 cardToolbarFrame, -(cardFrame.origin.x + cardTabOriginOffset.dx),
1806 -(cardFrame.origin.y + cardTabOriginOffset.dy));
1807
1808 // Calculate colors for the crossfade.
1809 UIColor* cardBackgroundColor =
1810 [self isCurrentSetIncognito]
1811 ? [UIColor colorWithWhite:kCardFrameBackgroundBrightnessIncognito
1812 alpha:1.0]
1813 : [UIColor colorWithWhite:kCardFrameBackgroundBrightness alpha:1.0];
1814 UIColor* toolbarBackgroundColor = cardBackgroundColor;
1815 if ([self.transitionToolbarController
1816 isKindOfClass:[NewTabPageToolbarController class]]) {
1817 // Use white for the non-incognito NTP toolbar.
1818 toolbarBackgroundColor = [UIColor whiteColor];
1819 } else if (self.transitionToolbarController.backgroundView.hidden ||
1820 self.transitionToolbarController.backgroundView.alpha == 0) {
1821 // If the background view isn't visible, use the base toolbar view's
1822 // background color.
1823 toolbarBackgroundColor =
1824 self.transitionToolbarController.view.backgroundColor;
1825 }
1826
1827 // Create frame animation.
1828 CFTimeInterval duration = ios::material::kDuration1;
1829 CAMediaTimingFunction* timingFunction =
1830 ios::material::TimingFunction(ios::material::CurveEaseInOut);
1831 BOOL isPresentingStackView =
1832 (transitionStyle == STACK_TRANSITION_STYLE_PRESENTING);
1833 CGRect beginFrame =
1834 isPresentingStackView ? screenToolbarFrame : cardToolbarFrame;
1835 CGRect endFrame =
1836 isPresentingStackView ? cardToolbarFrame : screenToolbarFrame;
1837 CAAnimation* frameAnimation = FrameAnimationMake(
1838 self.dummyToolbarBackgroundView.layer, beginFrame, endFrame);
1839 frameAnimation.duration = duration;
1840 frameAnimation.timingFunction = timingFunction;
1841
1842 // Create color animation.
1843 UIColor* beginColor =
1844 isPresentingStackView ? toolbarBackgroundColor : cardBackgroundColor;
1845 UIColor* endColor =
1846 isPresentingStackView ? cardBackgroundColor : toolbarBackgroundColor;
1847 CABasicAnimation* colorAnimation =
1848 [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
1849 colorAnimation.fromValue = reinterpret_cast<id>(beginColor.CGColor);
1850 colorAnimation.toValue = reinterpret_cast<id>(endColor.CGColor);
1851 colorAnimation.fillMode = kCAFillModeBoth;
1852 colorAnimation.removedOnCompletion = NO;
1853 colorAnimation.duration = duration;
1854 colorAnimation.timingFunction = timingFunction;
1855
1856 // Create corner radius animation.
1857 CGFloat toolbarCornerRadius = toolbarView.layer.cornerRadius;
1858 CGFloat beginCornerRadius =
1859 isPresentingStackView ? toolbarCornerRadius : kCardFrameCornerRadius;
1860 CGFloat endCornerRadius =
1861 isPresentingStackView ? kCardFrameCornerRadius : toolbarCornerRadius;
1862 CABasicAnimation* cornerRadiusAnimation =
1863 [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
1864 cornerRadiusAnimation.fromValue = @(beginCornerRadius);
1865 cornerRadiusAnimation.toValue = @(endCornerRadius);
1866 cornerRadiusAnimation.fillMode = kCAFillModeBoth;
1867 cornerRadiusAnimation.removedOnCompletion = NO;
1868 cornerRadiusAnimation.duration = duration;
1869 cornerRadiusAnimation.timingFunction = timingFunction;
1870
1871 // Add animations.
1872 CAAnimation* animation = AnimationGroupMake(
1873 @[ frameAnimation, colorAnimation, cornerRadiusAnimation ]);
1874 [self.dummyToolbarBackgroundView.layer
1875 addAnimation:animation
1876 forKey:kDummyToolbarBackgroundViewAnimationKey];
1877 }
1878
1879 - (void)reverseDummyToolbarBackgroundViewAnimation {
1880 ReverseAnimationsForKeyForLayers(kDummyToolbarBackgroundViewAnimationKey,
1881 @[ self.dummyToolbarBackgroundView.layer ]);
1882 }
1883
1884 - (void)animateTransitionToolbarWithCardFrame:(CGRect)cardFrame
1885 transitionStyle:
1886 (StackTransitionStyle)transitionStyle {
1887 // Add the transition toolbar and update its frame.
1888 CGFloat toolbarHeight =
1889 self.transitionToolbarController.view.frame.size.height;
1890 [_activeCardSet.displayView
1891 insertSubview:self.transitionToolbarController.view
1892 aboveSubview:_activeCardSet.currentCard.view];
1893 CGRect toolbarFrame =
1894 [_activeCardSet.displayView convertRect:[_toolbarController view].frame
1895 fromView:self.view];
1896 CGFloat heightDifference = toolbarFrame.size.height - toolbarHeight;
1897 toolbarFrame.origin.y += heightDifference;
1898 toolbarFrame.size.height -= heightDifference;
1899 self.transitionToolbarController.view.frame = toolbarFrame;
1900
1901 // The toolbar should animate such that its frame interpolates between the
1902 // normal toolbar frame at the top of the screen and the frame of the current
1903 // card's tab view.
1904 CGRect screenToolbarFrame = self.transitionToolbarController.view.frame;
1905 CGFloat cardTabHeight = kCardImageInsets.top - kCardFrameInset;
1906 CGRect cardToolbarFrame =
1907 CGRectInset(cardFrame, kCardFrameInset, kCardFrameInset);
1908 cardToolbarFrame.size.height = cardTabHeight;
1909
1910 // Add animations.
1911 BOOL isPresentingStackView =
1912 (transitionStyle == STACK_TRANSITION_STYLE_PRESENTING);
1913 ToolbarTransitionStyle style = isPresentingStackView
1914 ? TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW
1915 : TOOLBAR_TRANSITION_STYLE_TO_BVC;
1916 CGRect beginFrame =
1917 isPresentingStackView ? screenToolbarFrame : cardToolbarFrame;
1918 CGRect endFrame =
1919 isPresentingStackView ? cardToolbarFrame : screenToolbarFrame;
1920 [self.transitionToolbarController animateTransitionWithBeginFrame:beginFrame
1921 endFrame:endFrame
1922 transitionStyle:style];
1923 }
1924
1925 - (void)dismissWithSelectedTabAnimation {
1926 if (_isBeingDismissed || _activeCardSet.closingCard ||
1927 !_activeCardSet.cards.count) {
1928 return;
1929 }
1930 DCHECK(_isActive);
1931 [self prepareForDismissal];
1932 _isBeingDismissed = YES;
1933 // Once the stack view is starting to be dismissed, stop loading cards in the
1934 // background.
1935 [NSObject cancelPreviousPerformRequestsWithTarget:self];
1936
1937 [_delegate tabSwitcher:self
1938 dismissTransitionWillStartWithActiveModel:_activeCardSet.tabModel];
1939
1940 [self animateTransitionWithStyle:STACK_TRANSITION_STYLE_DISMISSING];
1941 }
1942
1943 - (std::vector<LayoutRect>)cardTransitionLayouts {
1944 std::vector<LayoutRect> cardLayouts;
1945 UIView* activeSetView = _activeCardSet.displayView;
1946
1947 // Setting a card's layout to |fullscreenLayout| will scale the content
1948 // snapshot such that it will fill the entire portion of the screen below the
1949 // toolbar. Used for the current card.
1950 LayoutRect fullscreenLayout = LayoutRectZero;
1951 fullscreenLayout.boundingWidth = CGRectGetWidth(activeSetView.bounds);
1952 fullscreenLayout.position.leading = -UIEdgeInsetsGetLeading(kCardImageInsets);
1953 fullscreenLayout.position.originY = -kCardImageInsets.top;
1954 fullscreenLayout.size.width = fullscreenLayout.boundingWidth +
1955 kCardImageInsets.left + kCardImageInsets.right;
1956 fullscreenLayout.size.height = CGRectGetHeight(activeSetView.bounds) +
1957 kCardImageInsets.top + kCardImageInsets.bottom;
1958
1959 // Cards above the current card (in z-index terms) should start/end offscreen.
1960 // Also account for the shadow so that the shadows cast by offscreen cards are
1961 // not visible at the beginning/end of the animation.
1962 CGFloat viewportLength =
1963 [self scrollLength:activeSetView.bounds.size] + kCardShadowThickness;
1964 LayoutRect offscreenLayout = fullscreenLayout;
1965 if (IsPortrait()) {
1966 offscreenLayout.position.originY += viewportLength + kCardImageInsets.top;
1967 } else {
1968 offscreenLayout.position.leading +=
1969 viewportLength + UIEdgeInsetsGetLeading(kCardImageInsets);
1970 }
1971
1972 // Cards below the current card (in z-index terms) should be top-aligned with
1973 // the toolbar and at the final card size.
1974 LayoutRect cardLayout = LayoutRectZero;
1975 cardLayout.boundingWidth = CGRectGetWidth(activeSetView.bounds);
1976 cardLayout.size = [self cardSize];
1977 cardLayout.position.leading = ios_internal::page_animation_util::kCardMargin;
1978 cardLayout.position.originY = -kCardImageInsets.top;
1979
1980 for (StackCard* card in _activeCardSet.cards) {
1981 if (card == _activeCardSet.currentCard) {
1982 // Current card takes the full screen.
1983 cardLayout = fullscreenLayout;
1984 } else if (LayoutRectEqualToRect(cardLayout, fullscreenLayout)) {
1985 // The card after the current card animates from off screen.
1986 cardLayout = offscreenLayout;
1987 }
1988 cardLayouts.push_back(cardLayout);
1989 }
1990
1991 return cardLayouts;
1992 }
1993
1994 - (Tab*)dismissWithNewTabAnimationToModel:(TabModel*)targetModel
1995 withURL:(const GURL&)url
1996 atIndex:(NSUInteger)position
1997 transition:(ui::PageTransition)transition {
1998 if (_isBeingDismissed)
1999 return NULL;
2000 if ([_activeCardSet tabModel] != targetModel)
2001 [self setActiveCardSet:[self inactiveCardSet]];
2002 return [self dismissWithNewTabAnimation:url
2003 atIndex:position
2004 transition:transition];
2005 }
2006
2007 - (void)setLastTapPoint:(id)sender {
2008 UIView* parentView = nil;
2009 CGPoint center;
2010 if ([sender isKindOfClass:[UIView class]]) {
2011 center = [sender center];
2012 parentView = [sender superview];
2013 }
2014 if ([sender isKindOfClass:[ToolsMenuViewItem class]]) {
2015 parentView = [[sender tableViewCell] superview];
2016 center = [[sender tableViewCell] center];
2017 }
2018
2019 if (parentView) {
2020 CGPoint viewCoordinate = [parentView convertPoint:center toView:self.view];
2021 _lastTapPoint = viewCoordinate;
2022 }
2023 }
2024
2025 - (Tab*)dismissWithNewTabAnimation:(const GURL&)URL
2026 atIndex:(NSUInteger)position
2027 transition:(ui::PageTransition)transition {
2028 // This helps smooth out the animation.
2029 [[_scrollView layer] setShouldRasterize:YES];
2030 if (_isBeingDismissed)
2031 return NULL;
2032 DCHECK(_isActive);
2033 [self prepareForDismissal];
2034 _isBeingDismissed = YES;
2035 [self setNeedsStatusBarAppearanceUpdate];
2036 DCHECK(URL.is_valid());
2037 // Stop pre-loading cards.
2038 [NSObject cancelPreviousPerformRequestsWithTarget:self];
2039
2040 // This uses a custom animation, so ignore the change that would be triggered
2041 // by adding a new tab to the model. This is left on since the stack view is
2042 // going away at this point, so staying in sync doesn't matter any more.
2043 [_activeCardSet setIgnoresTabModelChanges:YES];
2044 if (position == NSNotFound)
2045 position = [_activeCardSet.tabModel count];
2046 DCHECK(position <= [_activeCardSet.tabModel count]);
2047
2048 Tab* tab = [_activeCardSet.tabModel insertOrUpdateTabWithURL:URL
2049 referrer:web::Referrer()
2050 transition:transition
2051 windowName:nil
2052 opener:nil
2053 openedByDOM:NO
2054 atIndex:position
2055 inBackground:NO];
2056 [_activeCardSet.tabModel setCurrentTab:tab];
2057
2058 [_delegate tabSwitcher:self
2059 dismissTransitionWillStartWithActiveModel:_activeCardSet.tabModel];
2060
2061 CGFloat statusBarHeight = StatusBarHeight();
2062 CGRect viewBounds, remainder;
2063 CGRectDivide([self.view bounds], &remainder, &viewBounds, statusBarHeight,
2064 CGRectMinYEdge);
2065 UIImageView* newCard =
2066 [[[UIImageView alloc] initWithFrame:viewBounds] autorelease];
2067 // Temporarily resize the tab's view to ensure it matches the card while
2068 // generating a snapshot, but then restore the original frame.
2069 CGRect originalTabFrame = [tab view].frame;
2070 [tab view].frame = viewBounds;
2071 newCard.image = [tab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
2072 [tab view].frame = originalTabFrame;
2073 newCard.center =
2074 CGPointMake(CGRectGetMidX(viewBounds), CGRectGetMidY(viewBounds));
2075 [self.view addSubview:newCard];
2076
2077 void (^completionBlock)(void) = ^{
2078 [newCard removeFromSuperview];
2079 [[_scrollView layer] setShouldRasterize:NO];
2080 [_delegate tabSwitcherDismissTransitionDidEnd:self];
2081 };
2082
2083 CGPoint origin = _lastTapPoint;
2084 _lastTapPoint = CGPointZero;
2085 ios_internal::page_animation_util::AnimateInPaperWithAnimationAndCompletion(
2086 newCard, -statusBarHeight,
2087 newCard.frame.size.height - newCard.image.size.height, origin,
2088 [self isCurrentSetIncognito], nil, completionBlock);
2089 // TODO(stuartmorgan): Animate the other set off to the side.
2090
2091 return tab;
2092 }
2093
2094 #pragma mark UIGestureRecognizerDelegate methods
2095
2096 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)recognizer
2097 shouldReceiveTouch:(UITouch*)touch {
2098 // Don't swallow any touches while the tools popup menu is open.
2099 if ([_toolbarController toolsPopupController])
2100 return NO;
2101
2102 if ((recognizer == _pinchRecognizer) ||
2103 (recognizer == _swipeGestureRecognizer.get()))
2104 return YES;
2105
2106 // Only the mode switch recognizer should be triggered in the inactive deck
2107 // region (and it should only be triggered there).
2108 CGPoint touchLocation = [touch locationInView:_scrollView];
2109 BOOL inInactiveDeckRegion =
2110 CGRectContainsPoint([self inactiveDeckRegion], touchLocation);
2111 if (recognizer == _modeSwitchRecognizer.get())
2112 return inInactiveDeckRegion;
2113 else if (inInactiveDeckRegion)
2114 return NO;
2115
2116 // Extract the card on which the touch is occurring.
2117 CardView* cardView = nil;
2118 StackCard* card = nil;
2119 if (recognizer == _swipeDismissesCardRecognizer.get()) {
2120 UIView* activeView = _activeCardSet.displayView;
2121 CGPoint locationInActiveView = [touch locationInView:activeView];
2122 NSUInteger cardIndex = [self indexOfCardAtPoint:locationInActiveView];
2123 // |_swipeDismissesCardRecognizer| is interested only in touches that are
2124 // on cards in the active set.
2125 if (cardIndex == NSNotFound)
2126 return NO;
2127 DCHECK(cardIndex < [[_activeCardSet cards] count]);
2128 card = [[_activeCardSet cards] objectAtIndex:cardIndex];
2129 // This case seems like it should never happen, but it can be easily
2130 // handled anyway.
2131 if (![card viewIsLive])
2132 return YES;
2133 cardView = card.view;
2134 } else {
2135 // The recognizer is one of those attached to the card.
2136 DCHECK([recognizer.view isKindOfClass:[CardView class]]);
2137 cardView = (CardView*)recognizer.view;
2138 card = [self cardForView:cardView];
2139 }
2140
2141 // Prevent taps/presses in an uncollapsed card's close button from being
2142 // swallowed by the swipe-triggers-dismissal long press recognizer or
2143 // the card's tap/long press recognizer.
2144 if (CGRectContainsPoint([cardView closeButtonFrame],
2145 [touch locationInView:cardView]) &&
2146 card && ![_activeCardSet cardIsCollapsed:card])
2147 return NO;
2148
2149 return YES;
2150 }
2151
2152 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2153 shouldRecognizeSimultaneouslyWithGestureRecognizer:
2154 (UIGestureRecognizer*)otherGestureRecognizer {
2155 // Pinch and scroll must be allowed to recognize simultaneously to enable
2156 // smooth transitioning between scrolling and pinching.
2157 BOOL pinchRecognizerInvolved = (gestureRecognizer == _pinchRecognizer ||
2158 otherGestureRecognizer == _pinchRecognizer);
2159 BOOL scrollRecognizerInvolved =
2160 (gestureRecognizer == _scrollGestureRecognizer ||
2161 otherGestureRecognizer == _scrollGestureRecognizer);
2162 if (pinchRecognizerInvolved && scrollRecognizerInvolved)
2163 return YES;
2164
2165 // Swiping must be allowed to recognize simultaneously with the recognizer of
2166 // long presses that turn ambiguous swipes into card dismissals.
2167 BOOL swipeRecognizerInvolved =
2168 (gestureRecognizer == _swipeGestureRecognizer ||
2169 otherGestureRecognizer == _swipeGestureRecognizer);
2170 BOOL swipeDismissesCardRecognizerInvolved =
2171 (gestureRecognizer == _swipeDismissesCardRecognizer.get() ||
2172 otherGestureRecognizer == _swipeDismissesCardRecognizer.get());
2173 if (swipeRecognizerInvolved && swipeDismissesCardRecognizerInvolved)
2174 return YES;
2175
2176 // The swipe-triggers-card-dismissal long press recognizer must be allowed to
2177 // recognize simultaneously with the cards' long press recognizers that
2178 // trigger show-more-of-card.
2179 BOOL longPressRecognizerInvolved =
2180 ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] ||
2181 [otherGestureRecognizer
2182 isKindOfClass:[UILongPressGestureRecognizer class]]);
2183 if (swipeDismissesCardRecognizerInvolved && longPressRecognizerInvolved)
2184 return YES;
2185
2186 return NO;
2187 }
2188
2189 #pragma mark Action Handlers
2190
2191 - (void)closeTab:(id)sender {
2192 // Don't close any tabs mid-dismissal.
2193 if (_isBeingDismissed)
2194 return;
2195
2196 // Remove the frame animation before adding the fade out animation.
2197 DCHECK([sender isKindOfClass:[CardView class]]);
2198 CardView* cardView = static_cast<CardView*>(sender);
2199 [cardView removeFrameAnimation];
2200
2201 base::RecordAction(UserMetricsAction("MobileStackViewCloseTab"));
2202 StackCard* card = [self cardForView:cardView];
2203 DCHECK(card);
2204 NSUInteger tabIndex = [_activeCardSet.cards indexOfObject:card];
2205 if (tabIndex == NSNotFound)
2206 return;
2207
2208 // TODO(blundell): Crashes have been seen wherein |tabIndex| is out of bounds
2209 // of the TabModel's array. It is not currently understood how this case
2210 // occurs. To work around these crashes, close the tab only if it is indeed
2211 // the tab that corresponds to this card; otherwise, remove the card directly
2212 // without modifying the tab model. b/8321162
2213 BOOL cardCorrespondsToTab = NO;
2214 if (tabIndex < [_activeCardSet.tabModel count]) {
2215 Tab* tab = [_activeCardSet.tabModel tabAtIndex:tabIndex];
2216 cardCorrespondsToTab = (card.tabID == reinterpret_cast<NSUInteger>(tab));
2217 }
2218
2219 _activeCardSet.closingCard = card;
2220 if (cardCorrespondsToTab) {
2221 [_activeCardSet.tabModel closeTabAtIndex:tabIndex];
2222 } else {
2223 if (tabIndex < [_activeCardSet.tabModel count])
2224 DLOG(ERROR) << "Closed a card that didn't match the tab at its index";
2225 else
2226 DLOG(ERROR) << "Closed card at an index out of range of the tab model";
2227 [_activeCardSet removeCardAtIndex:tabIndex];
2228 }
2229 }
2230
2231 - (void)handleLongPressFrom:(UIGestureRecognizer*)recognizer {
2232 DCHECK(!_isBeingDismissed);
2233 DCHECK(_isActive);
2234
2235 if (recognizer == _swipeDismissesCardRecognizer.get())
2236 return;
2237
2238 UIGestureRecognizerState state = [recognizer state];
2239 if (state != UIGestureRecognizerStateBegan)
2240 return;
2241 if ([recognizer numberOfTouches] == 0)
2242 return;
2243
2244 // Don't take action on a card that is in the inactive stack, collapsed, or
2245 // the last card.
2246 CardView* cardView = (CardView*)recognizer.view;
2247 StackCard* card = [self cardForView:cardView];
2248 DCHECK(card);
2249 NSUInteger cardIndex = [[_activeCardSet cards] indexOfObject:card];
2250 DCHECK(cardIndex != NSNotFound);
2251 NSUInteger numCards = [[_activeCardSet cards] count];
2252 UIView* activeView = _activeCardSet.displayView;
2253
2254 if ([cardView superview] != activeView ||
2255 [_activeCardSet cardIsCollapsed:card] || cardIndex == (numCards - 1))
2256 return;
2257
2258 // Defer hiding the views of any cards that will be covered after the scroll
2259 // until the animation completes, as otherwise these cards immediately
2260 // disappear at the start of the animation.
2261 _activeCardSet.defersCardHiding = YES;
2262 [UIView animateWithDuration:kDefaultAnimationDuration
2263 animations:^{
2264 [_activeCardSet scrollCardAtIndex:cardIndex + 1 awayFromNeighbor:YES];
2265 }
2266 completion:^(BOOL finished) {
2267 _activeCardSet.defersCardHiding = NO;
2268 }];
2269 }
2270
2271 - (void)handlePinchFrom:(UIPinchGestureRecognizer*)recognizer {
2272 DCHECK(!_isBeingDismissed);
2273 DCHECK(_isActive);
2274 UIView* currentView = _activeCardSet.displayView;
2275 DCHECK(recognizer.view == currentView);
2276
2277 [_gestureStateTracker setPinching:YES];
2278 // Disable scrollView scrolling while a pinch is occurring. If the user lifts
2279 // a finger while pinching, callbacks to |handlePinchFrom:| will continue to
2280 // be made, and the code below will ensure that the cards get scrolled
2281 // properly.
2282 // TODO(blundell): Try to figure out how to re-enable deceleration for
2283 // such scrolls. b/5976932
2284 if ([_scrollView isScrollEnabled]) {
2285 [_scrollView setScrollEnabled:NO];
2286 _ignoreScrollCallbacks = YES;
2287 [self recenterScrollViewIfNecessary];
2288 }
2289
2290 UIGestureRecognizerState state = [recognizer state];
2291 if ((state == UIGestureRecognizerStateCancelled) ||
2292 (state == UIGestureRecognizerStateEnded)) {
2293 [_gestureStateTracker setScrollingInPinch:NO];
2294 [self pinchEnded];
2295 _ignoreScrollCallbacks = NO;
2296 [_scrollView setScrollEnabled:YES];
2297 [_gestureStateTracker setPinching:NO];
2298 return;
2299 }
2300
2301 DCHECK((state == UIGestureRecognizerStateBegan) ||
2302 (state == UIGestureRecognizerStateChanged));
2303
2304 CardStackPinchGestureRecognizer* pinchGestureRecognizer =
2305 base::mac::ObjCCastStrict<CardStackPinchGestureRecognizer>(recognizer);
2306 if ([pinchGestureRecognizer numberOfActiveTouches] < 2) {
2307 // Clear the pinch card indices so that they are refetched if the user puts
2308 // a second finger back down.
2309 [_gestureStateTracker setFirstPinchCardIndex:NSNotFound];
2310 [_gestureStateTracker setSecondPinchCardIndex:NSNotFound];
2311
2312 // The recognizer may continue to register two touches for a short period
2313 // after one of the touches is no longer active. Wait until there is only
2314 // one touch to be sure of accessing the information for the right touch.
2315 if ([recognizer numberOfTouches] != 1) {
2316 return;
2317 }
2318
2319 CGPoint fingerLocation =
2320 [_pinchRecognizer locationOfTouch:0 inView:currentView];
2321 CGFloat fingerOffset = [self scrollOffsetAmountForPoint:fingerLocation];
2322 if (![_gestureStateTracker scrollingInPinch]) {
2323 NSUInteger scrolledIndex = [self indexOfCardAtPoint:fingerLocation];
2324 if (scrolledIndex != NSNotFound) {
2325 // Begin the scroll.
2326 [_gestureStateTracker setScrollCardIndex:scrolledIndex];
2327 [_gestureStateTracker setPreviousFirstPinchOffset:fingerOffset];
2328 [_gestureStateTracker setPreviousScrollTime:(base::TimeTicks::Now())];
2329 [_gestureStateTracker setScrollingInPinch:YES];
2330 // Animate back overpinch as necessary.
2331 [self pinchEnded];
2332 }
2333 return;
2334 }
2335
2336 // Perform the scroll.
2337 CGFloat delta =
2338 fingerOffset - [_gestureStateTracker previousFirstPinchOffset];
2339 NSInteger scrolledIndex = [_gestureStateTracker scrollCardIndex];
2340 DCHECK(scrolledIndex != NSNotFound);
2341 [_activeCardSet scrollCardAtIndex:scrolledIndex
2342 byDelta:delta
2343 allowEarlyOverscroll:YES
2344 decayOnOverscroll:YES
2345 scrollLeadingCards:YES];
2346 [_gestureStateTracker setPreviousFirstPinchOffset:fingerOffset];
2347 [_gestureStateTracker updateScrollVelocityWithScrollDistance:delta];
2348 [_gestureStateTracker setPreviousScrollTime:(base::TimeTicks::Now())];
2349 return;
2350 }
2351
2352 [_gestureStateTracker setScrollingInPinch:NO];
2353
2354 DCHECK([recognizer numberOfTouches] >= 2);
2355 // Extract first and second offsets of the pinch.
2356 CGPoint firstPinchPoint = [recognizer locationOfTouch:0 inView:currentView];
2357 CGPoint secondPinchPoint = [recognizer locationOfTouch:1 inView:currentView];
2358 if ([self scrollOffsetAmountForPoint:firstPinchPoint] >
2359 [self scrollOffsetAmountForPoint:secondPinchPoint]) {
2360 CGPoint temp = firstPinchPoint;
2361 firstPinchPoint = secondPinchPoint;
2362 secondPinchPoint = temp;
2363 }
2364 CGFloat firstOffset = [self scrollOffsetAmountForPoint:firstPinchPoint];
2365 CGFloat secondOffset = [self scrollOffsetAmountForPoint:secondPinchPoint];
2366 NSInteger firstPinchCardIndex = [_gestureStateTracker firstPinchCardIndex];
2367 NSInteger secondPinchCardIndex = [_gestureStateTracker secondPinchCardIndex];
2368
2369 // Pinch does not actually cause cards to move until user has started moving
2370 // fingers with each finger on a distinct card.
2371 if ((state == UIGestureRecognizerStateBegan) ||
2372 (firstPinchCardIndex == NSNotFound) ||
2373 (secondPinchCardIndex == NSNotFound) ||
2374 (firstPinchCardIndex == secondPinchCardIndex)) {
2375 [_gestureStateTracker
2376 setFirstPinchCardIndex:[self indexOfCardAtPoint:firstPinchPoint]];
2377 [_gestureStateTracker
2378 setSecondPinchCardIndex:[self indexOfCardAtPoint:secondPinchPoint]];
2379 [_gestureStateTracker setPreviousFirstPinchOffset:firstOffset];
2380 [_gestureStateTracker setPreviousSecondPinchOffset:secondOffset];
2381 return;
2382 }
2383
2384 DCHECK(firstPinchCardIndex != NSNotFound);
2385 DCHECK(secondPinchCardIndex != NSNotFound);
2386 DCHECK(firstPinchCardIndex < secondPinchCardIndex);
2387
2388 CGFloat firstDelta =
2389 firstOffset - [_gestureStateTracker previousFirstPinchOffset];
2390 CGFloat secondDelta =
2391 secondOffset - [_gestureStateTracker previousSecondPinchOffset];
2392 [_activeCardSet.stackModel handleMultitouchWithFirstDelta:firstDelta
2393 secondDelta:secondDelta
2394 firstCardIndex:firstPinchCardIndex
2395 secondCardIndex:secondPinchCardIndex
2396 decayOnOverpinch:YES];
2397
2398 [_activeCardSet updateCardVisibilities];
2399
2400 [_gestureStateTracker setPreviousFirstPinchOffset:firstOffset];
2401 [_gestureStateTracker setPreviousSecondPinchOffset:secondOffset];
2402 }
2403
2404 - (void)handleTapFrom:(UITapGestureRecognizer*)recognizer {
2405 DCHECK(!_isBeingDismissed);
2406 DCHECK(_isActive);
2407 if (recognizer.state != UIGestureRecognizerStateEnded)
2408 return;
2409
2410 if (recognizer == _modeSwitchRecognizer.get()) {
2411 DCHECK(CGRectContainsPoint([self inactiveDeckRegion],
2412 [recognizer locationInView:_scrollView]));
2413 [self setActiveCardSet:[self inactiveCardSet]];
2414 return;
2415 }
2416
2417 CardView* cardView = (CardView*)recognizer.view;
2418 UIView* activeView = _activeCardSet.displayView;
2419 if ([cardView superview] != activeView)
2420 return;
2421
2422 // Don't open the card if it's collapsed behind its succeeding neighbor, as
2423 // this was likely just a misplaced tap in that case.
2424 StackCard* card = [self cardForView:cardView];
2425 // TODO(blundell) : The card should not be nil here, see b/6759862.
2426 if (!card || [_activeCardSet cardIsCollapsed:card])
2427 return;
2428
2429 DCHECK(!CGRectContainsPoint([cardView closeButtonFrame],
2430 [recognizer locationInView:cardView]));
2431
2432 if (self.transitionStyle == STACK_TRANSITION_STYLE_NONE) {
2433 [_activeCardSet setCurrentCard:card];
2434 [self dismissWithSelectedTabAnimation];
2435 } else if ([card isEqual:_activeCardSet.currentCard]) {
2436 // If the currently selected card is tapped mid-presentation animation,
2437 // simply reverse the animation immediately if it hasn't already been
2438 // reversed.
2439 if (self.transitionStyle != STACK_TRANSITION_STYLE_DISMISSING)
2440 [self cancelTransitionAnimation];
2441 } else {
2442 // If a new card is tapped mid-presentation, store a reference to the card
2443 // so that it can be selected upon dismissal in the presentation's
2444 // completion block.
2445 self.transitionTappedCard = card;
2446 }
2447 }
2448
2449 - (void)handlePanFrom:(UIPanGestureRecognizer*)gesture {
2450 DCHECK(!_isBeingDismissed);
2451 DCHECK(_isActive);
2452 // Check if the gesture's initial state needs to be set.
2453 if (gesture.state == UIGestureRecognizerStateBegan ||
2454 [_gestureStateTracker resetSwipedCardOnNextSwipe]) {
2455 // Save first position to be able to calculate swipe distances later.
2456 BOOL isPortrait =
2457 UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2458 // TODO(crbug.com/228456): All of the swipe code should be operating on the
2459 // swipe's first touch in order to avoid the "dancing swipe" bug. Note that
2460 // this might involve adding some checks to handle the case where the number
2461 // of touches in the swipe is 0.
2462 CGPoint point = [gesture locationInView:_scrollView];
2463 [_gestureStateTracker
2464 setSwipeStartingPosition:(isPortrait ? point.x : point.y)];
2465
2466 // Save the index of the card on which the swipe started (if any).
2467 CGPoint activePoint = [gesture locationInView:_activeCardSet.displayView];
2468 NSUInteger cardIndex = [self indexOfCardAtPoint:activePoint];
2469 [_gestureStateTracker setSwipedCardIndex:cardIndex];
2470
2471 [_gestureStateTracker setResetSwipedCardOnNextSwipe:NO];
2472 // Signal that the swipe is beginning so that the type of the swipe will be
2473 // calculated on the next callback (note that it is too early to calculate
2474 // it here as the direction of the swipe, which is a component used in the
2475 // calculation, is currently unknown).
2476 [_gestureStateTracker setSwipeIsBeginning:YES];
2477 // Determine whether a swipe of ambiguous intent should change decks or
2478 // dismiss a card.
2479 UIGestureRecognizerState state = [_swipeDismissesCardRecognizer state];
2480 BOOL ambiguousSwipeChangesDecks =
2481 (state != UIGestureRecognizerStateBegan &&
2482 state != UIGestureRecognizerStateChanged);
2483 [_gestureStateTracker
2484 setAmbiguousSwipeChangesDecks:ambiguousSwipeChangesDecks];
2485 return;
2486 }
2487
2488 if ([_gestureStateTracker swipeIsBeginning]) {
2489 [self determineSwipeType:gesture];
2490 [_gestureStateTracker setSwipeIsBeginning:NO];
2491 }
2492
2493 if ([_gestureStateTracker swipeChangesDecks]) {
2494 [self swipeDeck:gesture];
2495 return;
2496 }
2497
2498 // Check whether the swipe is actually on a card.
2499 NSUInteger cardIndex = [_gestureStateTracker swipedCardIndex];
2500 if (cardIndex == NSNotFound) {
2501 [_gestureStateTracker setResetSwipedCardOnNextSwipe:YES];
2502 return;
2503 }
2504 // Take action only if the card being swiped is not collapsed.
2505 DCHECK(cardIndex < [[_activeCardSet cards] count]);
2506 StackCard* card = [[_activeCardSet cards] objectAtIndex:cardIndex];
2507 if ([_activeCardSet cardIsCollapsed:card]) {
2508 [_gestureStateTracker setResetSwipedCardOnNextSwipe:YES];
2509 return;
2510 }
2511 // Remove the transition toolbar controller from the view hierarchy if the
2512 // card swipe occurs while a transition animation is occurring.
2513 if ([self.transitionToolbarController.view isDescendantOfView:self.view])
2514 [self.transitionToolbarController.view removeFromSuperview];
2515 [self swipeCard:gesture];
2516 }
2517
2518 - (void)determineSwipeType:(UIPanGestureRecognizer*)gesture {
2519 if (![self bothDecksShouldBeDisplayed]) {
2520 [_gestureStateTracker setSwipeChangesDecks:NO];
2521 return;
2522 }
2523
2524 if ([_gestureStateTracker swipedCardIndex] == NSNotFound) {
2525 [_gestureStateTracker setSwipeChangesDecks:YES];
2526 return;
2527 }
2528
2529 // The swipe is on a card. Check whether the intent of the swipe is
2530 // ambiguous.
2531 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2532 CGPoint swipePoint = [gesture locationInView:_scrollView];
2533 CGFloat swipePosition = isPortrait ? swipePoint.x : swipePoint.y;
2534 CGFloat swipeStartingPosition = [_gestureStateTracker swipeStartingPosition];
2535 CGFloat swipeDistance = swipePosition - swipeStartingPosition;
2536 if (UseRTLLayout() && isPortrait)
2537 swipeDistance *= -1;
2538 BOOL mainSetActive = (_activeCardSet == _mainCardSet);
2539 BOOL swipeIntentIsAmbiguous = (mainSetActive && swipeDistance < 0.0) ||
2540 (!mainSetActive && swipeDistance > 0.0);
2541
2542 BOOL swipeChangesDecks =
2543 swipeIntentIsAmbiguous ? [_gestureStateTracker ambiguousSwipeChangesDecks]
2544 : NO;
2545 [_gestureStateTracker setSwipeChangesDecks:swipeChangesDecks];
2546 }
2547
2548 - (CGFloat)distanceForSwipeToTriggerAction {
2549 return ([self scrollBreadth:[self view].bounds.size] *
2550 kSwipeCardScreenFraction);
2551 }
2552
2553 - (BOOL)swipeShouldTriggerAction:(CGFloat)endingPosition {
2554 CGFloat swipeStartingPosition = [_gestureStateTracker swipeStartingPosition];
2555 CGFloat threshold = [self distanceForSwipeToTriggerAction];
2556 return std::abs(endingPosition - swipeStartingPosition) > threshold;
2557 }
2558
2559 - (void)swipeDeck:(UIPanGestureRecognizer*)gesture {
2560 // StateBegan is handled by the caller handlePanFrom.
2561 DCHECK(gesture.state != UIGestureRecognizerStateBegan);
2562 // Swiping between decks should only be invoked when more than one deck is
2563 // being displayed.
2564 DCHECK([self bothDecksShouldBeDisplayed]);
2565
2566 CGPoint point = [gesture locationInView:_scrollView];
2567 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2568 CGFloat position = isPortrait ? point.x : point.y;
2569 CGFloat swipeStartingPosition = [_gestureStateTracker swipeStartingPosition];
2570
2571 // In portrait on RTL, the incognito card stack is laid out to the left of
2572 // the maint stack, so invert the pan direction.
2573 BOOL shouldInvert = UseRTLLayout() && isPortrait;
2574
2575 if (gesture.state == UIGestureRecognizerStateChanged) {
2576 CGFloat offset = position - swipeStartingPosition;
2577 // Decay drag if going off screen to mimic UIScrollView's bounce.
2578 BOOL isIncognito = [self isCurrentSetIncognito];
2579 if ((isIncognito && swipeStartingPosition > position) ||
2580 (!isIncognito && swipeStartingPosition < position))
2581 offset /= 2;
2582 if (shouldInvert)
2583 offset *= -1;
2584 [self updateDeckAxisPositionsWithShiftAmount:offset];
2585
2586 } else if (gesture.state == UIGestureRecognizerStateEnded) {
2587 if ([self swipeShouldTriggerAction:position]) {
2588 // |topLeftCardSet| is the card set on the left in portrait and on top in
2589 // landscape, and |bottomRightCardSet| is the card set on the right in
2590 // portrait and the bottom in landscape. If |position| is greater than
2591 // |swipeStartingPosition|, the gesture is dragging |topLeftCardSet| into
2592 // view. Otherwise, |bottomRightCardSet| is being dragged into view. Can't
2593 // just flip the active card set because this swipe might be a bounce that
2594 // is leaving the active card set unchanged.
2595 CardSet* topLeftCardSet = shouldInvert ? _otrCardSet : _mainCardSet;
2596 CardSet* bottomRightCardSet = shouldInvert ? _mainCardSet : _otrCardSet;
2597 _activeCardSet = position > swipeStartingPosition ? topLeftCardSet
2598 : bottomRightCardSet;
2599 }
2600 [self displayActiveCardSet];
2601 }
2602 }
2603
2604 - (void)swipeCard:(UIPanGestureRecognizer*)gesture {
2605 // StateBegan is handled by the caller handlePanFrom.
2606 DCHECK(gesture.state != UIGestureRecognizerStateBegan);
2607
2608 CGPoint point = [gesture locationInView:_scrollView];
2609 BOOL isPortrait = UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2610 CGFloat position = isPortrait ? point.x : point.y;
2611 CGFloat swipeStartingPosition = [_gestureStateTracker swipeStartingPosition];
2612 CGFloat distanceMoved = fabs(position - swipeStartingPosition);
2613
2614 // Calculate fractions of animation breadth to trigger dismissal that have
2615 // been covered so far.
2616 CGFloat fractionOfAnimationBreadth =
2617 distanceMoved /
2618 ios_internal::page_animation_util::AnimateOutTransformBreadth();
2619 // User can potentially move their finger further than animation breath/
2620 // dismissal threshold distance. Ensure that these corner cases don't cause
2621 // any unexpected behavior.
2622 fractionOfAnimationBreadth =
2623 std::min(fractionOfAnimationBreadth, CGFloat(1.0));
2624
2625 // Calculate direction of the swipe.
2626 BOOL clockwise = position - swipeStartingPosition > 0;
2627 if (!isPortrait)
2628 clockwise = !clockwise;
2629
2630 NSUInteger swipedCardIndex = [_gestureStateTracker swipedCardIndex];
2631 StackCard* card = [_activeCardSet.cards objectAtIndex:swipedCardIndex];
2632 _activeCardSet.closingCard = card;
2633
2634 if (gesture.state == UIGestureRecognizerStateChanged) {
2635 // Transform card along |AnimateOutTransform| by the fraction of the
2636 // animation breadth that has been covered so far.
2637 [card view].transform =
2638 ios_internal::page_animation_util::AnimateOutTransform(
2639 fractionOfAnimationBreadth, clockwise, isPortrait);
2640 // Fade the card to become transparent at the conclusion of the animation,
2641 // and the card's tab to become transparent at the time that the card
2642 // reaches the threshold for being dismissed.
2643 [card view].alpha = 1 - fractionOfAnimationBreadth;
2644 } else {
2645 if (gesture.state == UIGestureRecognizerStateEnded &&
2646 [self swipeShouldTriggerAction:position]) {
2647 // Track card if animation should dismiss in reverse from the norm of
2648 // clockwise in portrait, counter-clockwise in landscape.
2649 if ((isPortrait && !clockwise) || (!isPortrait && clockwise))
2650 _reverseDismissCard.reset([card retain]);
2651 // This will trigger the completion of the close card animation.
2652 [self closeTab:card.view];
2653 } else {
2654 // Animate back to starting position.
2655 [UIView animateWithDuration:kDefaultAnimationDuration
2656 delay:0
2657 options:UIViewAnimationCurveEaseOut
2658 animations:^{
2659 [card view].alpha = 1;
2660 [[card view] setTabOpacity:1];
2661 [card view].transform = CGAffineTransformIdentity;
2662 }
2663 completion:^(BOOL finished) {
2664 _activeCardSet.closingCard = nil;
2665 }];
2666 }
2667 }
2668 }
2669
2670 - (StackCard*)cardForView:(CardView*)view {
2671 // This isn't terribly efficient, but since it is only intended for use in
2672 // response to a user action it's not worth the bookkeeping of a reverse
2673 // mapping to make it constant time.
2674 for (StackCard* card in _activeCardSet.cards) {
2675 if (card.viewIsLive && card.view == view) {
2676 return card;
2677 }
2678 }
2679 return nil;
2680 }
2681
2682 - (IBAction)chromeExecuteCommand:(id)sender {
2683 int command = [sender tag];
2684
2685 switch (command) {
2686 case IDC_SHOW_TOOLS_MENU:
2687 [self showToolsMenuPopup];
2688 break;
2689 // Closing all while the main set is active closes everything, but closing
2690 // all while incognito is active only closes incognito tabs.
2691 case IDC_CLOSE_ALL_TABS:
2692 DCHECK(![self isCurrentSetIncognito]);
2693 [self removeAllCardsFromSet:_mainCardSet];
2694 [self removeAllCardsFromSet:_otrCardSet];
2695 break;
2696 case IDC_CLOSE_ALL_INCOGNITO_TABS:
2697 DCHECK([self isCurrentSetIncognito]);
2698 [self removeAllCardsFromSet:_activeCardSet];
2699 break;
2700 case IDC_NEW_INCOGNITO_TAB:
2701 case IDC_NEW_TAB:
2702 // Ensure that the right mode is showing.
2703 if ([self isCurrentSetIncognito] != (command == IDC_NEW_INCOGNITO_TAB))
2704 [self setActiveCardSet:[self inactiveCardSet]];
2705 [self setLastTapPoint:sender];
2706 [self dismissWithNewTabAnimation:GURL(kChromeUINewTabURL)
2707 atIndex:NSNotFound
2708 transition:ui::PAGE_TRANSITION_TYPED];
2709 break;
2710 case IDC_TOGGLE_TAB_SWITCHER:
2711 [self dismissWithSelectedTabAnimation];
2712 break;
2713 default:
2714 [super chromeExecuteCommand:sender];
2715 break;
2716 }
2717 }
2718
2719 - (void)showToolsMenuPopup {
2720 base::scoped_nsobject<ToolsMenuContext> context(
2721 [[ToolsMenuContext alloc] initWithDisplayView:[self view]]);
2722 [context setInTabSwitcher:YES];
2723 // When checking for the existence of tabs, catch the case where the main set
2724 // is both active and empty, but the incognito set has some cards.
2725 if (([[_activeCardSet cards] count] == 0) &&
2726 (_activeCardSet == _otrCardSet || [[_otrCardSet cards] count] == 0))
2727 [context setNoOpenedTabs:YES];
2728 if (_activeCardSet == _otrCardSet)
2729 [context setInIncognito:YES];
2730 [_toolbarController showToolsMenuPopupWithContext:context];
2731 }
2732
2733 #pragma mark Notification Handlers
2734
2735 - (void)allModelTabsHaveClosed:(NSNotification*)notify {
2736 // Early return if the stack view is not active. This can sometimes occur if
2737 // |clearInternalState| triggers the deletion of a tab model.
2738 if (!_isActive)
2739 return;
2740
2741 CardSet* closedSet =
2742 (notify.object == [_mainCardSet tabModel]) ? _mainCardSet : _otrCardSet;
2743
2744 // If the tabModel that send the notification is not one handled by one of
2745 // the two card sets, just return. This will happen when the otr tab model is
2746 // deleted because the incognito profile is deleted.
2747 if (notify.object != [closedSet tabModel])
2748 return;
2749
2750 if (closedSet == _activeCardSet)
2751 [self activeCardCountChanged];
2752 for (UIView* card in [closedSet.displayView subviews]) {
2753 [card removeFromSuperview];
2754 }
2755 [closedSet setIgnoresTabModelChanges:NO];
2756 // No need to re-sync with the card set here, since the tab model (and thus
2757 // the card set) is known to be empty.
2758
2759 // The animation of closing all the main set's cards interacts badly with the
2760 // animation of switching to main-card-set-only mode, so if the incognito set
2761 // finishes closing while the main set is still animating (in the case of
2762 // closing all cards at once) wait until the main set finishes before updating
2763 // the display (neccessary so the state is right if a new tab is opened).
2764 if ((closedSet == _otrCardSet && ![_mainCardSet ignoresTabModelChanges]) ||
2765 (closedSet == _mainCardSet && [[_otrCardSet cards] count] == 0)) {
2766 [self displayMainCardSetOnly];
2767 }
2768
2769 [_toolbarController setTabCount:[_activeCardSet.cards count]];
2770 }
2771
2772 #pragma mark CardSetObserver Methods
2773
2774 - (void)cardSet:(CardSet*)cardSet didAddCard:(StackCard*)newCard {
2775 [self updateScrollViewContentSize];
2776
2777 if (cardSet == _activeCardSet) {
2778 [self activeCardCountChanged];
2779 [_toolbarController setTabCount:[_activeCardSet.cards count]];
2780 }
2781
2782 // Place the card at the right destination point to animate in: staggered
2783 // from its previous neighbor if it is the last card, or in the location of
2784 // its successive neighbor (which will slide down to make room) otherwise.
2785 NSArray* cards = [cardSet cards];
2786 NSUInteger cardIndex = [cards indexOfObject:newCard];
2787 CGFloat maxStagger = [[cardSet stackModel] maxStagger];
2788
2789 if (newCard == [cards lastObject]) {
2790 if ([cards count] == 1) {
2791 // Simply lay out the card.
2792 [cardSet fanOutCards];
2793 } else {
2794 StackCard* previousCard = [cards objectAtIndex:cardIndex - 1];
2795 BOOL isPortrait =
2796 UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2797 LayoutRect layout = previousCard.layout;
2798 if (isPortrait)
2799 layout.position.originY += maxStagger;
2800 else
2801 layout.position.leading += maxStagger;
2802 newCard.layout = layout;
2803 }
2804
2805 } else {
2806 DCHECK(cardIndex != NSNotFound);
2807 DCHECK(cardIndex + 1 < [cards count]);
2808 newCard.layout = [[cards objectAtIndex:cardIndex + 1] layout];
2809 }
2810
2811 // Animate the new card in at its destination location.
2812 ios_internal::page_animation_util::AnimateInCardWithAnimationAndCompletion(
2813 newCard.view, NULL, NULL);
2814
2815 // Set up the animation of the existing cards.
2816 NSUInteger indexToScroll = cardIndex + 1;
2817 CGFloat scrollAmount = maxStagger;
2818 if (newCard == [cards lastObject] ||
2819 [cardSet isCardInEndStaggerRegion:newCard]) {
2820 // No scrolling actually needs to be done, although |scrollCardAtIndex:|
2821 // still has to be called if the new card is not in the start stack in
2822 // order to ensure that the end stack is re-laid out if necessary.
2823 indexToScroll = cardIndex;
2824 scrollAmount = 0;
2825 }
2826
2827 // If the new card is in the start stack, just re-lay out the start stack.
2828 // Otherwise, slide down the successive cards to make room and/or re-lay out
2829 // the end stack. TODO(blundell): The animation is behaving incorrectly when
2830 // the card being inserted is near the end stack: sometimes the slide down
2831 // doesn't occur, and sometimes it overscrolls, causing a visible bounce.
2832 void (^stackAnimation)(void) = ^{
2833 if ([cardSet isCardInStartStaggerRegion:newCard]) {
2834 [cardSet layOutStartStack];
2835 } else {
2836 [cardSet scrollCardAtIndex:indexToScroll
2837 byDelta:scrollAmount
2838 allowEarlyOverscroll:NO
2839 decayOnOverscroll:NO
2840 scrollLeadingCards:YES];
2841 }
2842 };
2843
2844 cardSet.defersCardHiding = YES;
2845 [UIView animateWithDuration:kDefaultAnimationDuration
2846 delay:0
2847 options:UIViewAnimationOptionBeginFromCurrentState
2848 animations:stackAnimation
2849 completion:^(BOOL) {
2850 cardSet.defersCardHiding = NO;
2851 if (cardSet == _activeCardSet) {
2852 // Ensure that state is properly reset if there was a
2853 // scroll/pinch occurring.
2854 [self scrollEnded];
2855 }
2856 }];
2857 }
2858
2859 - (void)cardSet:(CardSet*)cardSet
2860 willRemoveCard:(StackCard*)cardBeingRemoved
2861 atIndex:(NSUInteger)index {
2862 // All handlers working on that card must be stopped to prevent concurrency
2863 // and/or UI inconcistencies.
2864 if (cardSet == _activeCardSet) {
2865 // Cancel any outstanding gestures that were tracking a card index, as they
2866 // might have been operating on cards that no longer exist. Ideally, these
2867 // events would allow the gestures to continue and just reset the cards on
2868 // which they are operating. However, doing that correctly in all cases
2869 // proves near-impossible: if the call to -disableGestureHandlers happens
2870 // slightly too late, then a gesture callback could occur in the new state
2871 // of the world with the gesture still operating on the old state of the
2872 // world. Meanwhile if the call to -enableGestureHandlers happens
2873 // slightly too early, then a gesture callback could occur while still in
2874 // the old state of the world, meaning that the card being tracked would
2875 // revert to the old (problematic) card.
2876 [self disableGestureHandlers];
2877 }
2878 }
2879
2880 - (void)cardSet:(CardSet*)cardSet
2881 didRemoveCard:(StackCard*)removedCard
2882 atIndex:(NSUInteger)index {
2883 if (cardSet == _activeCardSet) {
2884 // Reenable the gesture handlers (disabled in
2885 // -cardSet:willRemoveCard:atIndex). It is now safe to do so as the card
2886 // that was being removed has been removed at this point.
2887 [self enableGestureHandlers];
2888 [_toolbarController setTabCount:[_activeCardSet.cards count]];
2889 }
2890
2891 // If no view was ever created for this card, it's too late to make one. This
2892 // can only happen if a tab is closed by something other than user action,
2893 // and even then only if the card hasn't been pre-loaded yet, so not doing th
2894 // animation isn't a problem.
2895 if (removedCard.viewIsLive) {
2896 // Determine what direction animation should rotate in. The norm is
2897 // clockwise in portrait, counter-clockwise in landscape; however, it needs
2898 // to be reversed if this animation is occurring as the conclusion of a
2899 // swipe that went in the opposite direction from the norm.
2900 BOOL isPortrait =
2901 UIInterfaceOrientationIsPortrait(_lastInterfaceOrientation);
2902 BOOL clockwise = isPortrait ? _reverseDismissCard != removedCard
2903 : _reverseDismissCard == removedCard;
2904 [self animateOutCardView:removedCard.view
2905 delay:0
2906 clockwise:clockwise
2907 completion:nil];
2908 // Reset |reverseDismissCard| if that card was the one dismissed.
2909 if ((isPortrait && !clockwise) || (!isPortrait && clockwise))
2910 _reverseDismissCard.reset();
2911 }
2912 // Nil out the the closing card after all closing animations have finished.
2913 [CATransaction begin];
2914 [CATransaction setCompletionBlock:^{
2915 cardSet.closingCard = nil;
2916 }];
2917 // If the last incognito card closes, switch back to just the main set.
2918 if ([cardSet.cards count] == 0 && cardSet == _otrCardSet.get()) {
2919 [self displayMainCardSetOnly];
2920 } else {
2921 NSUInteger numCards = [[cardSet cards] count];
2922 if (numCards == 0) {
2923 // Commit the transaction before early return.
2924 [CATransaction commit];
2925 return;
2926 }
2927 if (index == numCards) {
2928 // If the card that was closed was the last card and was in the start
2929 // stack, the start stack might need to be re-laid out to show a
2930 // previously hidden card.
2931 if ([cardSet overextensionTowardStartOnCardAtIndex:numCards - 1]) {
2932 [UIView animateWithDuration:kDefaultAnimationDuration
2933 animations:^{
2934 [cardSet layOutStartStack];
2935 }];
2936 }
2937 } else {
2938 // Scroll up the card following the removed card to be placed where the
2939 // removed card was.
2940 LayoutRectPosition removedCardPosition = removedCard.layout.position;
2941 LayoutRectPosition followingCardPosition =
2942 [[cardSet cards][index] layout].position;
2943 CGFloat scrollAmount =
2944 [self scrollOffsetAmountForPosition:removedCardPosition] -
2945 [self scrollOffsetAmountForPosition:followingCardPosition];
2946 [cardSet updateShadowLayout];
2947 [UIView animateWithDuration:kDefaultAnimationDuration
2948 animations:^{
2949 [cardSet scrollCardAtIndex:index
2950 byDelta:scrollAmount
2951 allowEarlyOverscroll:YES
2952 decayOnOverscroll:NO
2953 scrollLeadingCards:NO];
2954 [cardSet updateShadowLayout];
2955 }];
2956 }
2957 }
2958 [CATransaction commit];
2959 }
2960
2961 - (void)cardSet:(CardSet*)cardSet displayedCard:(StackCard*)card {
2962 // Add gesture recognizers to the card.
2963 [card.view addCardCloseTarget:self action:@selector(closeTab:)];
2964 [card.view addAccessibilityTarget:self
2965 action:@selector(accessibilityFocusedOnElement:)];
2966
2967 base::scoped_nsobject<UIGestureRecognizer> tapRecognizer([
2968 [UITapGestureRecognizer alloc] initWithTarget:self
2969 action:@selector(handleTapFrom:)]);
2970 tapRecognizer.get().delegate = self;
2971 [card.view addGestureRecognizer:tapRecognizer.get()];
2972
2973 base::scoped_nsobject<UIGestureRecognizer> longPressRecognizer(
2974 [[UILongPressGestureRecognizer alloc]
2975 initWithTarget:self
2976 action:@selector(handleLongPressFrom:)]);
2977 longPressRecognizer.get().delegate = self;
2978 [card.view addGestureRecognizer:longPressRecognizer.get()];
2979 }
2980
2981 - (void)cardSetRecreatedCards:(CardSet*)cardSet {
2982 // Remove the old card views, if any, then start loading the new ones.
2983 for (UIView* card in [cardSet.displayView subviews]) {
2984 [card removeFromSuperview];
2985 }
2986 [self preloadCardViewsAsynchronously];
2987 }
2988
2989 #pragma mark -
2990
2991 // The following method is based on Apple sample code available at
2992 // http://developer.apple.com/library/ios/samplecode/StreetScroller/
2993 // Introduction/Intro.html.
2994 - (void)recenterScrollViewIfNecessary {
2995 CGFloat contentOffset =
2996 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]];
2997 CGFloat contentLength = [self scrollLength:[_scrollView contentSize]];
2998 CGFloat viewportLength = [self scrollLength:[_scrollView bounds].size];
2999 DCHECK(contentLength > viewportLength || [_activeCardSet.cards count] == 0);
3000
3001 CGFloat centerOffset = (contentLength - viewportLength) / 2.0;
3002 CGFloat distanceFromCenter = fabs(contentOffset - centerOffset);
3003
3004 if (distanceFromCenter > centerOffset / 2.0) {
3005 _ignoreScrollCallbacks = YES;
3006 [_scrollView
3007 setContentOffset:[self scrollOffsetPointWithAmount:centerOffset]];
3008 [self alignDisplayViewsToViewport];
3009 _ignoreScrollCallbacks = NO;
3010 }
3011 }
3012
3013 - (void)alignDisplayViewsToViewport {
3014 DCHECK(CGSizeEqualToSize([_mainCardSet displayView].frame.size,
3015 [_scrollView frame].size));
3016 DCHECK(CGSizeEqualToSize([_otrCardSet displayView].frame.size,
3017 [_scrollView frame].size));
3018 CGRect newDisplayViewFrame = CGRectMake(
3019 [_scrollView contentOffset].x, [_scrollView contentOffset].y,
3020 [_scrollView frame].size.width, [_scrollView frame].size.height);
3021 [_mainCardSet displayView].frame = newDisplayViewFrame;
3022 [_otrCardSet displayView].frame = newDisplayViewFrame;
3023 }
3024
3025 // Caps overscroll once the stack becomes fully overextended or deceleration
3026 // slows below a given velocity to achieve a nice-looking bounce effect.
3027 - (BOOL)shouldEndScroll {
3028 if ([[_activeCardSet cards] count] == 0 ||
3029 ![_activeCardSet stackIsOverextended] || ![_scrollView isDecelerating])
3030 return NO;
3031 if ([_activeCardSet stackIsFullyOverextended])
3032 return YES;
3033 NSUInteger lastCardIndex = [[_activeCardSet cards] count] - 1;
3034 // Kill the scroll in the case where a fling wasn't detected early enough,
3035 // resulting in part of the stack being overscrolled toward the start without
3036 // the whole stack being overscrolled toward the start.
3037 if ([_activeCardSet overextensionTowardStartOnCardAtIndex:0] &&
3038 ![_activeCardSet overextensionTowardStartOnCardAtIndex:lastCardIndex])
3039 return YES;
3040 return [_gestureStateTracker scrollVelocity] < kMinFlingVelocityInOverscroll;
3041 }
3042
3043 - (void)scrollEnded {
3044 if ([_activeCardSet stackIsOverextended]) {
3045 void (^toDoWhenDone)(void) = ^{
3046 [self recenterScrollViewIfNecessary];
3047 };
3048 [self animateOverextensionEliminationWithCompletion:toDoWhenDone];
3049 } else {
3050 [self recenterScrollViewIfNecessary];
3051 }
3052 }
3053
3054 - (void)pinchEnded {
3055 BOOL scrollingInPinch = [_gestureStateTracker scrollingInPinch];
3056
3057 if (![_activeCardSet stackIsOverextended]) {
3058 if (!scrollingInPinch)
3059 [self recenterScrollViewIfNecessary];
3060 return;
3061 }
3062
3063 if (scrollingInPinch) {
3064 NSUInteger scrollCardIndex = [_gestureStateTracker scrollCardIndex];
3065 DCHECK(scrollCardIndex != NSNotFound);
3066 if ([_activeCardSet overextensionOnCardAtIndex:scrollCardIndex])
3067 return;
3068 }
3069
3070 void (^toDoWhenDone)(void) = NULL;
3071 if (!scrollingInPinch) {
3072 toDoWhenDone = ^{
3073 [self recenterScrollViewIfNecessary];
3074 };
3075 }
3076 [self animateOverextensionEliminationWithCompletion:toDoWhenDone];
3077 }
3078
3079 - (void)animateOverextensionEliminationWithCompletion:
3080 (ProceduralBlock)completion {
3081 _activeCardSet.defersCardHiding = YES;
3082 [UIView animateWithDuration:kOverextensionEliminationAnimationDuration
3083 delay:0
3084 options:(UIViewAnimationOptionAllowUserInteraction |
3085 UIViewAnimationOptionOverrideInheritedCurve |
3086 UIViewAnimationOptionCurveEaseOut)
3087 animations:^{
3088 [_activeCardSet eliminateOverextension];
3089 }
3090 completion:^(BOOL finished) {
3091 _activeCardSet.defersCardHiding = NO;
3092 if (completion)
3093 completion();
3094 }];
3095 }
3096
3097 - (void)killScrollDeceleration {
3098 _ignoreScrollCallbacks = YES;
3099 [_scrollView setContentOffset:[_scrollView contentOffset] animated:NO];
3100 // The above call does not always generate a callback to
3101 // |scrollViewDidScroll:|, so it is necessary to update the gesture state
3102 // tracker's previous scroll offset explicitly here.
3103 [_gestureStateTracker
3104 setPreviousScrollOffset:
3105 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]]];
3106 _ignoreScrollCallbacks = NO;
3107 }
3108
3109 // To mimic standard iOS behavior on overscroll, the stack is allowed to
3110 // overscroll approximately half of the screen length on drag and a lesser
3111 // amount on fling.
3112 - (void)adjustMaximumOverextensionAmount:(BOOL)scrollIsFling {
3113 CGFloat screenLength = [self scrollLength:self.view.bounds.size];
3114 CGFloat maximumOverextensionAmount =
3115 scrollIsFling ? [_activeCardSet overextensionAmount] + screenLength / 4.0
3116 : screenLength / 2.0;
3117
3118 // The overextension amount is not allowed to grow after a fling begins, as
3119 // otherwise the fling would just keep overextending further and further.
3120 if (scrollIsFling &&
3121 maximumOverextensionAmount > [_activeCardSet maximumOverextensionAmount])
3122 return;
3123 [_activeCardSet setMaximumOverextensionAmount:maximumOverextensionAmount];
3124 }
3125
3126 #pragma mark UIScrollViewDelegate methods
3127
3128 - (void)scrollViewDidScroll:(UIScrollView*)scrollView {
3129 DCHECK(!_isBeingDismissed);
3130 DCHECK(_isActive);
3131 DCHECK(scrollView == _scrollView);
3132 // Whether this callback will trigger a scroll or not, have to ensure that
3133 // the display views' positions are updated after any change in the scroll
3134 // view's content offset.
3135 [self alignDisplayViewsToViewport];
3136
3137 if (_ignoreScrollCallbacks) {
3138 [_gestureStateTracker
3139 setPreviousScrollOffset:
3140 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]]];
3141 return;
3142 }
3143
3144 if ([[_activeCardSet cards] count] == 0)
3145 return;
3146
3147 // First check if the scrolled card needs to be reset. Have to be careful to
3148 // do this only when the user is actually starting a new scroll.
3149 if ([_scrollView isTracking] &&
3150 [_gestureStateTracker resetScrollCardOnNextDrag]) {
3151 CGPoint fingerLocation =
3152 [_scrollGestureRecognizer locationOfTouch:0
3153 inView:_activeCardSet.displayView];
3154 [_gestureStateTracker
3155 setScrollCardIndex:[self indexOfCardAtPoint:fingerLocation]];
3156 // In certain corner cases the previous scroll offset is not up-to-date
3157 // when the scrolled card needs to be reset (most notably, when rotating
3158 // the device while scrolling). Below provides a fix for these cases
3159 // without harming other cases.
3160 [_gestureStateTracker
3161 setPreviousScrollOffset:
3162 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]]];
3163 [_gestureStateTracker setPreviousScrollTime:(base::TimeTicks::Now())];
3164 [_gestureStateTracker setResetScrollCardOnNextDrag:NO];
3165 }
3166
3167 CGFloat contentOffset =
3168 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]];
3169 CGFloat delta = contentOffset - [_gestureStateTracker previousScrollOffset];
3170
3171 // If overscrolled and in a fling, compute the delta to apply manually to
3172 // achieve a nice-looking deceleration effect.
3173 if ([_activeCardSet stackIsOverextended] && ![_scrollView isTracking]) {
3174 CGFloat currentVelocity = [_gestureStateTracker scrollVelocity];
3175 CGFloat elapsedTime = CGFloat(
3176 (base::TimeTicks::Now() - [_gestureStateTracker previousScrollTime])
3177 .InMilliseconds());
3178 if (elapsedTime > 0.0) {
3179 CGFloat sign = (delta >= 0) ? 1.0 : -1.0;
3180 CGFloat distanceAtCurrentVelocity = sign * currentVelocity * elapsedTime;
3181 delta = distanceAtCurrentVelocity * kDecayFactorInBounce;
3182 }
3183 }
3184 [_gestureStateTracker updateScrollVelocityWithScrollDistance:delta];
3185
3186 if ([self shouldEndScroll]) {
3187 [self killScrollDeceleration];
3188 return;
3189 }
3190
3191 // Perform the scroll.
3192 NSInteger scrolledIndex = [_gestureStateTracker scrollCardIndex];
3193 if (scrolledIndex == NSNotFound) {
3194 // User is scrolling outside the active card stack. Ideally, this scroll
3195 // would be ignored; however, that is challenging to implement properly
3196 // (in particular, continuing to ensure that scroll view is recentered
3197 // when it needs to be). For now, pick a reasonable index to do the
3198 // scrolling on. TODO(blundell): Figure out how to ignore these scrolls
3199 // while ensuring that scroll view is recentered as necessary. b/5858386
3200 scrolledIndex = 0;
3201 if (delta > 0)
3202 scrolledIndex = [[_activeCardSet cards] count] - 1;
3203 // On the next scroll, check again so that if the user starts scrolling on
3204 // a card, the scroll moves to be on that card.
3205 [_gestureStateTracker setResetScrollCardOnNextDrag:YES];
3206 }
3207
3208 DCHECK(scrolledIndex != NSNotFound);
3209
3210 // Scrolls that are greater than a given velocity are assumed to be flings
3211 // even if the user's finger is still registered as being down, as it is
3212 // extremely likely that the user is actually in the middle of doing a fling
3213 // motion (and if the scrolled card is allowed to visibly overscroll before
3214 // the stack is fully collapsed, the ability to handle the fling as a fling
3215 // from a UI perspective is lost). The latter heuristic has the cost of
3216 // sometimes ending up in the scrolled card not tracking the user's finger if
3217 // the user is scrolling very fast near the start stack.
3218 BOOL isFling =
3219 ![_scrollView isTracking] || ([_gestureStateTracker scrollVelocity] >
3220 kThresholdVelocityForTreatingScrollAsFling);
3221 [self adjustMaximumOverextensionAmount:isFling];
3222
3223 // The scroll view's content offset increases with scrolling toward the start
3224 // stack. These semantics are inverted from those of
3225 // |scrollCardAtIndex:byDelta:|. If the scroll is a drag, then overscroll
3226 // occurs with the scrolled card and the scroll decays once overscrolling
3227 // begins to mimic the native iOS behavior on overscroll. In the case of
3228 // fling, overscroll does not occur until the scroll is fully
3229 // collapsed/expanded and no decay occurs on overscroll as the delta has
3230 // already been manually adjusted in this case (see above).
3231 BOOL inverseFanDirection = UseRTLLayout() && !IsPortrait();
3232 if (inverseFanDirection) {
3233 // In landscape RTL layouts, StackCard's application of its layout values to
3234 // its underlying CardView is flipped across the midpoint Y axis, but the
3235 // scroll view maintains its non-RTL scrolling behavior. In this
3236 // situation, reverse the scrolling direction before applying it to the
3237 // CardSet.
3238 delta *= -1;
3239 }
3240 [_activeCardSet scrollCardAtIndex:scrolledIndex
3241 byDelta:-delta
3242 allowEarlyOverscroll:!isFling
3243 decayOnOverscroll:!isFling
3244 scrollLeadingCards:YES];
3245
3246 // Verify that if scroll view's content offset has hit a boundary point, the
3247 // active card stack is fully scrolled in the corresponding direction. Note
3248 // that this check intentionally doesn't take into account overextension: due
3249 // to the fact that overextension is a transient state, the stack is not
3250 // guaranteed to be fully overextended when these checks are performed (and
3251 // that is OK).
3252 DCHECK(contentOffset >= 0);
3253 CGFloat epsilon = std::numeric_limits<CGFloat>::epsilon();
3254 if (contentOffset < epsilon) {
3255 if (inverseFanDirection)
3256 DCHECK([_activeCardSet stackIsFullyCollapsed]);
3257 else
3258 DCHECK([_activeCardSet stackIsFullyFannedOut]);
3259 }
3260 CGFloat viewportLength = [self scrollLength:[_scrollView bounds].size];
3261 CGFloat contentSizeUpperLimit =
3262 [self scrollLength:[_scrollView contentSize]] - viewportLength;
3263 DCHECK(contentOffset <= contentSizeUpperLimit);
3264 if (std::abs(contentSizeUpperLimit - contentOffset) < epsilon) {
3265 if (inverseFanDirection)
3266 DCHECK([_activeCardSet stackIsFullyFannedOut]);
3267 else
3268 DCHECK([_activeCardSet stackIsFullyCollapsed]);
3269 }
3270
3271 [_gestureStateTracker setPreviousScrollTime:(base::TimeTicks::Now())];
3272 [_gestureStateTracker
3273 setPreviousScrollOffset:
3274 [self scrollOffsetAmountForPoint:[_scrollView contentOffset]]];
3275 }
3276
3277 - (void)scrollViewDidEndDragging:(UIScrollView*)scrollView
3278 willDecelerate:(BOOL)willDecelerate {
3279 DCHECK(scrollView == _scrollView);
3280 [_gestureStateTracker setResetScrollCardOnNextDrag:YES];
3281 // Recenter the scroll view's content offset after making sure that there is
3282 // no scrolling currently occurring.
3283 if (willDecelerate || [_scrollView isDragging] ||
3284 [_scrollView isDecelerating] || [_gestureStateTracker pinching])
3285 return;
3286 [self scrollEnded];
3287 }
3288
3289 - (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView {
3290 DCHECK(scrollView == _scrollView);
3291 // Recenter the scroll view's content offset after making sure that there is
3292 // no scrolling currently occurring (this deceleration might have been ended
3293 // by the user starting a new scroll).
3294 if ([_scrollView isDragging] || [_scrollView isDecelerating] ||
3295 [_gestureStateTracker pinching])
3296 return;
3297 [self scrollEnded];
3298 }
3299
3300 #pragma mark - Accessibility methods
3301
3302 // Handles scrolling through the card stack and scrolling between main and
3303 // incognito card stacks while in voiceover mode. Three finger scroll toward
3304 // an edge stack displays the next few collapsed tabs from the appropriate edge
3305 // stack. Three finger scroll toward the inactive stack switches between the
3306 // main and incognito card stacks, as appropriate.
3307 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
3308 BOOL isPortrait = IsPortrait();
3309 BOOL shouldScrollTowardEnd = NO;
3310 BOOL shouldScrollTowardStart = NO;
3311 BOOL shouldSwitchToMain = NO;
3312 BOOL shouldSwitchToIncognito = NO;
3313
3314 switch (direction) {
3315 case UIAccessibilityScrollDirectionDown:
3316 if (isPortrait) {
3317 shouldScrollTowardEnd = YES;
3318 } else {
3319 shouldSwitchToIncognito = YES;
3320 }
3321 break;
3322 case UIAccessibilityScrollDirectionUp:
3323 if (isPortrait) {
3324 shouldScrollTowardStart = YES;
3325 } else {
3326 shouldSwitchToMain = YES;
3327 }
3328 break;
3329 case UIAccessibilityScrollDirectionLeft:
3330 if (isPortrait) {
3331 shouldSwitchToIncognito = YES;
3332 } else {
3333 shouldScrollTowardEnd = YES;
3334 }
3335 break;
3336 case UIAccessibilityScrollDirectionRight:
3337 if (isPortrait) {
3338 shouldSwitchToMain = YES;
3339 } else {
3340 shouldScrollTowardStart = YES;
3341 }
3342 break;
3343 default:
3344 break;
3345 }
3346
3347 if (shouldScrollTowardEnd) {
3348 DCHECK([_activeCardSet.stackModel firstEndStackCardIndex] > -1);
3349 [_activeCardSet
3350 fanOutCardsWithStartIndex:
3351 std::min(
3352 (NSInteger)[_activeCardSet.cards count] - 1,
3353 (NSInteger)[_activeCardSet.stackModel firstEndStackCardIndex])];
3354 } else if (shouldScrollTowardStart) {
3355 DCHECK([_activeCardSet.stackModel lastStartStackCardIndex] > -1);
3356 [_activeCardSet
3357 fanOutCardsWithStartIndex:
3358 std::max((NSInteger)0,
3359 (NSInteger)(
3360 [_activeCardSet.stackModel lastStartStackCardIndex] -
3361 [_activeCardSet.stackModel fannedStackCount]))];
3362 } else if ([self bothDecksShouldBeDisplayed]) {
3363 if (shouldSwitchToMain) {
3364 [self setActiveCardSet:_mainCardSet];
3365 } else if (shouldSwitchToIncognito) {
3366 [self setActiveCardSet:_otrCardSet];
3367 }
3368 } else {
3369 return NO;
3370 }
3371
3372 NSUInteger firstCardIndex = std::max(
3373 [_activeCardSet.stackModel lastStartStackCardIndex], (NSInteger)0);
3374 StackCard* card = [_activeCardSet.cards objectAtIndex:firstCardIndex];
3375 [[card view] postAccessibilityNotification];
3376 [self postOpenTabsAccessibilityNotification];
3377
3378 return YES;
3379 }
3380
3381 // Posts accessibility notification that announces to the user which tabs are
3382 // currently visible on the screen.
3383 - (void)postOpenTabsAccessibilityNotification {
3384 if ([_activeCardSet.cards count] == 0) {
3385 return;
3386 }
3387
3388 NSInteger count = [_activeCardSet.cards count];
3389 NSInteger lastStartIndex =
3390 [_activeCardSet.stackModel lastStartStackCardIndex];
3391 DCHECK(lastStartIndex < (int)[_activeCardSet.cards count]);
3392 if (lastStartIndex < 0) {
3393 lastStartIndex = 0;
3394 }
3395 NSInteger firstEndIndex = [_activeCardSet.stackModel firstEndStackCardIndex];
3396 NSInteger first = lastStartIndex < 0 ? 1 : lastStartIndex + 1;
3397 NSInteger last = firstEndIndex < 0 ? count : firstEndIndex;
3398
3399 // Post notification to voiceover to read which tabs are currently visible.
3400 NSString* incognito = [self isCurrentSetIncognito] ? @"Incognito" : @"";
3401 NSString* firstVisible = [NSString stringWithFormat:@"%" PRIuNS, first];
3402 NSString* lastVisible = [NSString stringWithFormat:@"%" PRIuNS, last];
3403 NSString* numCards = [NSString stringWithFormat:@"%" PRIuNS, count];
3404 UIAccessibilityPostNotification(
3405 UIAccessibilityPageScrolledNotification,
3406 l10n_util::GetNSStringF(IDS_IOS_CARD_STACK_SCROLLED_NOTIFICATION,
3407 base::SysNSStringToUTF16(incognito),
3408 base::SysNSStringToUTF16(firstVisible),
3409 base::SysNSStringToUTF16(lastVisible),
3410 base::SysNSStringToUTF16(numCards)));
3411 }
3412
3413 // Returns the StackCard with |element| in its view hierarchy.
3414 // Handles CardView, TitleLabel, and CloseButton elements.
3415 - (StackCard*)cardForAccessibilityElement:(UIView*)element {
3416 DCHECK([element isKindOfClass:[CardView class]] ||
3417 [element isKindOfClass:[TitleLabel class]] ||
3418 [element isKindOfClass:[CloseButton class]]);
3419
3420 if ([element isKindOfClass:[CardView class]]) {
3421 for (StackCard* card in _activeCardSet.cards) {
3422 if (card.view == element) {
3423 return card;
3424 }
3425 }
3426 } else {
3427 for (StackCard* card in _activeCardSet.cards) {
3428 if (card.view == element.superview.superview) {
3429 return card;
3430 }
3431 }
3432 }
3433 return nil;
3434 }
3435
3436 - (void)accessibilityFocusedOnElement:(id)element {
3437 StackCard* card = [self cardForAccessibilityElement:element];
3438 DCHECK(card);
3439 NSInteger index = [_activeCardSet.cards indexOfObject:card];
3440
3441 if (![_activeCardSet.stackModel cardLabelCovered:card]) {
3442 return;
3443 }
3444
3445 if (index >= [_activeCardSet.stackModel firstEndStackCardIndex] - 1) {
3446 // If card is in the end stack, scroll it toward the start.
3447 [_activeCardSet scrollCardAtIndex:index awayFromNeighbor:NO];
3448 } else if (index == [_activeCardSet.stackModel lastStartStackCardIndex] - 1) {
3449 // If card is the last covered card in the start stack, scroll the last
3450 // start stack card away from the start stack to reveal it.
3451 [_activeCardSet scrollCardAtIndex:index + 1 awayFromNeighbor:YES];
3452 } else {
3453 // If the card is in the middle of a stack that is not the end stack, fan
3454 // the cards out starting with that card.
3455 [_activeCardSet fanOutCardsWithStartIndex:index];
3456 [card.view postAccessibilityNotification];
3457 [self postOpenTabsAccessibilityNotification];
3458 }
3459 }
3460
3461 #pragma mark - UIResponder
3462
3463 - (NSArray*)keyCommands {
3464 base::WeakNSObject<StackViewController> weakSelf(self);
3465
3466 // Block to execute a command from the |tag|.
3467 base::mac::ScopedBlock<void (^)(NSInteger)> execute(
3468 ^(NSInteger tag) {
3469 [weakSelf
3470 chromeExecuteCommand:[GenericChromeCommand commandWithTag:tag]];
3471 },
3472 base::scoped_policy::RETAIN);
3473
3474 return @[
3475 [UIKeyCommand cr_keyCommandWithInput:@"t"
3476 modifierFlags:UIKeyModifierCommand
3477 title:l10n_util::GetNSStringWithFixup(
3478 IDS_IOS_TOOLS_MENU_NEW_TAB)
3479 action:^{
3480 if ([weakSelf isCurrentSetIncognito])
3481 execute.get()(IDC_NEW_INCOGNITO_TAB);
3482 else
3483 execute.get()(IDC_NEW_TAB);
3484 }],
3485 [UIKeyCommand
3486 cr_keyCommandWithInput:@"n"
3487 modifierFlags:UIKeyModifierCommand | UIKeyModifierShift
3488 title:l10n_util::GetNSStringWithFixup(
3489 IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB)
3490 action:^{
3491 execute.get()(IDC_NEW_INCOGNITO_TAB);
3492 }],
3493 [UIKeyCommand cr_keyCommandWithInput:@"n"
3494 modifierFlags:UIKeyModifierCommand
3495 title:nil
3496 action:^{
3497 if ([weakSelf isCurrentSetIncognito])
3498 execute.get()(IDC_NEW_INCOGNITO_TAB);
3499 else
3500 execute.get()(IDC_NEW_TAB);
3501 }],
3502 ];
3503 }
3504
3505 @end
3506
3507 @implementation StackViewController (Testing)
3508
3509 - (UIScrollView*)scrollView {
3510 return _scrollView.get();
3511 }
3512
3513 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698