OLD | NEW |
(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 |
OLD | NEW |