Index: ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm |
diff --git a/ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm b/ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4dbebacbf6bb2d6dfc2a0163c18c5db5fbcdde5f |
--- /dev/null |
+++ b/ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm |
@@ -0,0 +1,1197 @@ |
+// Copyright 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h" |
+ |
+#include <algorithm> |
+#include <cmath> |
+ |
+#include "base/logging.h" |
+#include "ios/chrome/browser/ui/rtl_geometry.h" |
+#import "ios/chrome/browser/ui/stack_view/stack_card.h" |
+#import "ios/chrome/browser/ui/ui_util.h" |
+ |
+namespace { |
+ |
+// The maximum number of cards that should be staggered at a collapse point. |
+const NSInteger kMaxVisibleStaggerCount = 4; |
+// The amount that each of the staggered cards in a stack should be staggered |
+// when fully collapsed. |
+const CGFloat kMinStackStaggerAmount = 4.0; |
+// The amount that a card should overlap with a previous/subsequent card when |
+// it is extended the maximum distance away (e.g., after a multitouch event). |
+const CGFloat kFullyExtendedCardOverlap = 8.0; |
+// The amount that a card's position is allowed to drift toward overextension |
+// before the card is considered to be overextended (i.e., an epsilon to allow |
+// for floating-point imprecision). |
+const CGFloat kDistanceBeforeOverextension = 0.0001; |
+// The factor by which scroll is decayed on overscroll. |
+const CGFloat kOverextensionDecayFactor = 2.0; |
+// The amount by which a card is scrolled when asked to scroll it away from its |
+// preceding neighbor. |
+const CGFloat kScrollAwayFromNeighborAmount = 200; |
+ |
+} // namespace |
+ |
+@interface CardStackLayoutManager () |
+ |
+// Exposes |kMinStackStaggerAmount| for tests. |
+- (CGFloat)minStackStaggerAmount; |
+// Exposes |kScrollAwayFromNeighborAmount| for tests. |
+- (CGFloat)scrollCardAwayFromNeighborAmount; |
+// Returns the current start stack limit allowing for overextension as follows: |
+// - If the card at |index| is not overextended toward the start, returns |
+// |startLimit_|. |
+// - Otherwise, returns the value of the start limit such that the position of |
+// the card at |index| in the start stack is its current position (with the |
+// exception that the value is capped at |limitOfOverextensionTowardStart|). |
+- (CGFloat)startStackLimitAllowingForOverextensionOnCardAtIndex: |
+ (NSUInteger)index; |
+// Based on cards' current positions, |startLimit|, and |endLimit_|, caps cards |
+// that should be in the start and end stack. The reason that |startLimit| is |
+// a parameter is that the position of the start stack can change due to |
+// overextension. |
+- (void)layOutEdgeStacksWithStartLimit:(CGFloat)startLimit; |
+// Based on cards' current positions and |limit|, caps cards that should be in |
+// the start stack. The reason that |limit| is a parameter is that the desired |
+// position for the visual start of the start stack can change due to |
+// overextension. |
+- (void)layOutStartStackWithLimit:(CGFloat)limit; |
+// Positions the cards in the end stack based on |endLimit_|, leaving enough |
+// margin so that the last card in the stack has |kMinStackStaggerAmount| |
+// amount of visibility before |endLimit_|. |
+- (void)layOutEndStack; |
+// Computes the index of what should be the the inner boundary card in the |
+// indicated stack based on the current positions of the cards and the desired |
+// |visualStackLimit|. |
+- (NSInteger)computeEdgeStackBoundaryIndex:(BOOL)startStack |
+ withVisualStackLimit:(CGFloat)visualStackLimit; |
+// Computes what the origin of the inner boundary card in the indicated stack |
+// based on |visualStackLimit|. |
+- (CGFloat)computeEdgeStackInnerEdge:(BOOL)startStack |
+ withVisualStackLimit:(CGFloat)visualStackLimit; |
+// Fans out the cards in the end stack and then recalculates the end stack. |
+- (void)recomputeEndStack; |
+// Fans out the cards in the start stack/end stack to be |maxStagger_| away |
+// from each other, with the first card in the stack being the greater of |
+// |maxStagger_| and its current distance away from its neighboring non- |
+// collapsed card. |
+- (void)fanOutCardsInEdgeStack:(BOOL)startStack; |
+// Returns the distance separating the origin of the card at |firstIndex| from |
+// that of the card at |secondIndex|. |
+- (CGFloat)distanceBetweenCardAtIndex:(NSUInteger)firstIndex |
+ andCardAtIndex:(NSUInteger)secondIndex; |
+// Returns the minimum offset that the first card is allowed to over-extend to |
+// toward the start. |
+- (CGFloat)limitOfOverextensionTowardStart; |
+// Returns the maximum offset that the first card is allowed to overscroll to |
+// toward the end. |
+- (CGFloat)limitOfOverscrollTowardEnd; |
+// Caps overscroll toward start and end to maximum allowed amounts and re-lays |
+// out the start and end stacks. If |allowEarlyOverscroll| is |YES|, |
+// overscrolling is allowed to occur naturally on the scrolled card; otherwise, |
+// overscrolling is not allowed to occur until the stack is fully |
+// collapsed/fanned out. |
+- (void)capOverscrollWithScrolledIndex:(NSUInteger)scrolledIndex |
+ allowEarlyOverscroll:(BOOL)allowEarlyOverscroll; |
+// Caps overscroll toward end to maximum allowed amount. |
+- (void)capOverscrollTowardEnd; |
+// Moves the cards so that any overscroll is eliminated. |
+- (void)eliminateOverscroll; |
+// Moves the cards so that any overpinch is eliminated. |
+- (void)eliminateOverpinch; |
+// Returns the maximum amount that a card can be offset from a |
+// preceding/following card: |cardSize - kFullyExtendedCardOverlap|. |
+- (CGFloat)maximumCardSeparation; |
+// Returns the maximum offset that the card at |index| can have given the |
+// constraint that no card can start more than |
+// |maximumCardSeparation:| away from the previous card. |
+- (CGFloat)maximumOffsetForCardAtIndex:(NSInteger)index; |
+// Returns the offset that the card at |index| would have after calling |
+// |fanOutCardsWithStartIndex:0|. |
+- (CGFloat)cappedFanoutOffsetForCardAtIndex:(NSInteger)index; |
+// Moves the card at |index| by |amount| along the layout axis, centered in the |
+// other direction at layoutAxisPosition_. |
+- (void)moveCardAtIndex:(NSUInteger)index byAmount:(CGFloat)amount; |
+// Moves |card|'s layout by |amount| along the layout axis. |
+- (void)moveCard:(StackCard*)card byAmount:(CGFloat)amount; |
+// Moves each of the cards between |startIndex| and |endIndex| inclusive by |
+// |delta| along the layout axis. |
+- (void)moveCardsFromIndex:(NSUInteger)startIndex |
+ toIndex:(NSUInteger)endIndex |
+ byAmount:(CGFloat)amount; |
+ |
+// Moves each of the cards before/after |index| (as indicated by |toEnd|) |
+// by |amount| with the constraint that for a non-edge-stack card (and for |
+// cards in the start stack if |restoreFanOutInStartStack| is |YES|), the |
+// amount that the card is moved is decreased by the amount necessary to |
+// restore the separation between that card and its next/previous neighbor to |
+// |maxStagger_|. Assumes that the card at |index| has been moved by |amount| |
+// prior to calling this method. ` |
+- (void)moveCardsrestoringFanoutFromIndex:(NSUInteger)index |
+ toEnd:(BOOL)toEnd |
+ byAmount:(CGFloat)amount |
+ restoreFanOutInStartStack:(BOOL)restoreFanOutInStartStack; |
+// Moves the origin of the card at |index| to |offset| along the layout axis, |
+// centered in the other direction at layoutAxisPosition_. |
+- (void)moveOriginOfCardAtIndex:(NSUInteger)index toOffset:(CGFloat)offset; |
+// Returns |offset| modified as necessary to make sure that it is not too |
+// close or too far from the origin of its constraining neighbor (previous or |
+// next, as determined by |constrainingNeighborIsPrevious|). |
+- (CGFloat)constrainedOffset:(CGFloat)offset |
+ forCardAtIndex:(NSInteger)index |
+ constrainingNeighborIsPrevious:(BOOL)isPrevious; |
+// Moves the cards starting at |index| by an amount that decays from |
+// |drivingDelta| with each card that gets moved. |
+- (void)moveCardsStartingAtIndex:(NSInteger)index |
+ towardsEnd:(BOOL)towardsEnd |
+ withDrivingDelta:(CGFloat)delta; |
+// Moves the cards in-between |firstIndex| and |secondIndex > firstIndex| |
+// inclusive via a proportional blend of |firstDelta| and |secondDelta|. |
+- (void)blendOffsetsOfCardsBetweenFirstIndex:(NSInteger)firstIndex |
+ secondIndex:(NSInteger)secondIndex |
+ withFirstDelta:(CGFloat)firstDelta |
+ secondDelta:(CGFloat)secondDelta; |
+// Returns the length of |size| in the current layout direction. |
+- (CGFloat)layoutLength:(CGSize)size; |
+// Returns the offset of |position| in the current layout direction. |
+- (CGFloat)layoutOffset:(LayoutRectPosition)position; |
+// Returns the offset of |card| in the current layout direction. |
+- (CGFloat)cardOffsetOnLayoutAxis:(StackCard*)card; |
+// Returns the pixel offset relative to the first/last card in a fully |
+// compressed stack to show a card that is |countFromEdge| fram the start/end. |
+- (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge; |
+// Returns the pixel offset relative to the first/last card in a fully |
+// compressed stack where a card being pushed onto the stack should start |
+// moving the existing cards. |
+- (CGFloat)pushThresholdForIndexFromEdge:(NSInteger)countFromEdge; |
+// Controls whether the cards keep their views synchronized when updates are |
+// made to their frame/bounds/center. |
+- (void)setSynchronizeCardViews:(BOOL)synchronizeViews; |
+// Returns YES if |index| is in the start stack. |
+- (BOOL)isInStartStack:(NSUInteger)index; |
+// Returns YES if |index| is in the end stack. |
+- (BOOL)isInEndStack:(NSUInteger)index; |
+// Returns YES if |index| is in the start or end stack. |
+- (BOOL)isInEdgeStack:(NSUInteger)index; |
+ |
+@end |
+ |
+#pragma mark - |
+ |
+@implementation CardStackLayoutManager |
+ |
+@synthesize cardSize = cardSize_; |
+@synthesize maxStagger = maxStagger_; |
+@synthesize maximumOverextensionAmount = maximumOverextensionAmount_; |
+@synthesize endLimit = endLimit_; |
+@synthesize layoutAxisPosition = layoutAxisPosition_; |
+@synthesize startLimit = startLimit_; |
+@synthesize layoutIsVertical = layoutIsVertical_; |
+@synthesize lastStartStackCardIndex = lastStartStackCardIndex_; |
+@synthesize firstEndStackCardIndex = firstEndStackCardIndex_; |
+ |
+- (id)init { |
+ if ((self = [super init])) { |
+ cards_.reset([[NSMutableArray alloc] init]); |
+ layoutIsVertical_ = YES; |
+ lastStartStackCardIndex_ = -1; |
+ firstEndStackCardIndex_ = -1; |
+ } |
+ return self; |
+} |
+ |
+- (CGFloat)minStackStaggerAmount { |
+ return kMinStackStaggerAmount; |
+} |
+ |
+- (CGFloat)scrollCardAwayFromNeighborAmount { |
+ return kScrollAwayFromNeighborAmount; |
+} |
+ |
+- (void)setEndLimit:(CGFloat)endLimit { |
+ endLimit_ = endLimit; |
+ [self recomputeEndStack]; |
+} |
+ |
+- (void)addCard:(StackCard*)card { |
+ [self insertCard:card atIndex:[cards_ count]]; |
+} |
+ |
+- (void)insertCard:(StackCard*)card atIndex:(NSUInteger)index { |
+ card.size = cardSize_; |
+ [cards_ insertObject:card atIndex:index]; |
+} |
+ |
+- (void)removeCard:(StackCard*)card { |
+ // Update edge stack boundary indices if necessary. |
+ NSInteger cardIndex = [cards_ indexOfObject:card]; |
+ DCHECK(cardIndex != NSNotFound); |
+ if (cardIndex <= lastStartStackCardIndex_) |
+ lastStartStackCardIndex_ -= 1; |
+ if (cardIndex < firstEndStackCardIndex_) |
+ firstEndStackCardIndex_ -= 1; |
+ |
+ [cards_ removeObject:card]; |
+} |
+ |
+- (void)removeAllCards { |
+ lastStartStackCardIndex_ = -1; |
+ firstEndStackCardIndex_ = -1; |
+ [cards_ removeAllObjects]; |
+} |
+ |
+- (void)setCardSize:(CGSize)size { |
+ cardSize_ = size; |
+ NSUInteger i = 0; |
+ CGFloat previousFirstCardOffset = 0; |
+ CGFloat newFirstCardOffset = 0; |
+ for (StackCard* card in cards_.get()) { |
+ CGFloat offset = [self cardOffsetOnLayoutAxis:card]; |
+ card.size = cardSize_; |
+ CGFloat newOffset = offset; |
+ |
+ // Attempt to preserve card positions, but ensure that the deck starts |
+ // within overextension limits and that all cards not in the start stack are |
+ // within minimum/maximum separation limits of their preceding neighbors. |
+ if (i == 0) { |
+ newOffset = std::max(newOffset, [self limitOfOverextensionTowardStart]); |
+ newOffset = std::min(newOffset, [self limitOfOverscrollTowardEnd]); |
+ previousFirstCardOffset = offset; |
+ newFirstCardOffset = newOffset; |
+ } else if ((NSInteger)i <= lastStartStackCardIndex_) { |
+ // Preserve the layout of the start stack. |
+ newOffset = newFirstCardOffset + (offset - previousFirstCardOffset); |
+ } else { |
+ newOffset = [self constrainedOffset:newOffset |
+ forCardAtIndex:i |
+ constrainingNeighborIsPrevious:YES]; |
+ } |
+ |
+ [self moveOriginOfCardAtIndex:i toOffset:newOffset]; |
+ i++; |
+ } |
+} |
+ |
+- (void)setLayoutIsVertical:(BOOL)layoutIsVertical { |
+ if (layoutIsVertical_ == layoutIsVertical) |
+ return; |
+ layoutIsVertical_ = layoutIsVertical; |
+ // Restore the cards' positions along the new layout axis. |
+ for (NSUInteger i = 0; i < [cards_ count]; i++) { |
+ LayoutRectPosition position = [[cards_ objectAtIndex:i] layout].position; |
+ CGFloat prevLayoutAxisOffset = |
+ layoutIsVertical_ ? position.leading : position.originY; |
+ [self moveOriginOfCardAtIndex:i toOffset:prevLayoutAxisOffset]; |
+ } |
+} |
+ |
+- (void)setLayoutAxisPosition:(CGFloat)position { |
+ layoutAxisPosition_ = position; |
+ for (StackCard* card in cards_.get()) { |
+ LayoutRect layout = card.layout; |
+ if (layoutIsVertical_) |
+ layout.position.leading = position - 0.5 * layout.size.width; |
+ else |
+ layout.position.originY = position - 0.5 * layout.size.height; |
+ card.layout = layout; |
+ } |
+} |
+ |
+- (NSArray*)cards { |
+ return cards_; |
+} |
+ |
+- (void)fanOutCardsWithStartIndex:(NSUInteger)startIndex { |
+ NSUInteger numCards = [cards_ count]; |
+ if (numCards == 0) |
+ return; |
+ DCHECK(startIndex < numCards); |
+ |
+ // Temporarily turn off updates to the cards' views as this method might be |
+ // being called from within an animation, and updating the coordinates of a |
+ // |UIView| multiple times while it is animating can cause undesired |
+ // behavior. |
+ [self setSynchronizeCardViews:NO]; |
+ |
+ // Move the cards starting at |startIndex| into place. |
+ for (NSUInteger i = 0; i < numCards - startIndex; ++i) { |
+ // The start cap for this card, accounting for visual stacking. |
+ CGFloat uncappedPosition = i * maxStagger_ + startLimit_; |
+ [self moveOriginOfCardAtIndex:(startIndex + i) toOffset:uncappedPosition]; |
+ } |
+ |
+ // Fan out the cards behind the one at |startIndex|. |
+ for (NSInteger i = (startIndex - 1); i >= 0; --i) { |
+ CGFloat uncappedPosition = startLimit_ - (startIndex - i) * maxStagger_; |
+ [self moveOriginOfCardAtIndex:i toOffset:uncappedPosition]; |
+ } |
+ |
+ [self layOutEdgeStacksWithStartLimit:startLimit_]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (void)recomputeEndStack { |
+ [self setSynchronizeCardViews:NO]; |
+ if (firstEndStackCardIndex_ != -1) |
+ [self fanOutCardsInEdgeStack:NO]; |
+ [self layOutEndStack]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+// Starts the fan at the stack boundary if the neighboring non-collapsed card |
+// is at least |maxStagger_| away from the stack (note that due to pinching, |
+// the neighboring card can be an arbitrary distance away from the stack); |
+// otherwise, starts the fan at |maxStagger_| away from that neighboring |
+// non-collapsed card. |
+- (void)fanOutCardsInEdgeStack:(BOOL)startStack { |
+ NSUInteger numCards = [cards_ count]; |
+ if (numCards == 0) |
+ return; |
+ NSUInteger numCardsToMove; |
+ if (startStack) |
+ numCardsToMove = lastStartStackCardIndex_ + 1; |
+ else |
+ numCardsToMove = numCards - firstEndStackCardIndex_; |
+ |
+ if (numCardsToMove == 0) |
+ return; |
+ |
+ // Find the offset at which to start. |
+ NSUInteger stackBoundaryIndex = |
+ startStack ? lastStartStackCardIndex_ : firstEndStackCardIndex_; |
+ CGFloat startOffset = |
+ [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:stackBoundaryIndex]]; |
+ if ((startStack && stackBoundaryIndex < numCards - 1) || |
+ (!startStack && stackBoundaryIndex > 0)) { |
+ // Ensure that the stack is laid out starting at least |maxStagger_| |
+ // separation from the neighboring non-collapsed card. |
+ NSUInteger nonCollapsedLimitIndex = |
+ startStack ? stackBoundaryIndex + 1 : stackBoundaryIndex - 1; |
+ CGFloat nonCollapsedLimitOffset = [self |
+ cardOffsetOnLayoutAxis:[cards_ objectAtIndex:nonCollapsedLimitIndex]]; |
+ CGFloat distance = fabs(nonCollapsedLimitOffset - startOffset); |
+ if (distance < maxStagger_) { |
+ startOffset = startStack ? nonCollapsedLimitOffset - maxStagger_ |
+ : nonCollapsedLimitOffset + maxStagger_; |
+ } |
+ } |
+ |
+ NSUInteger currentIndex = stackBoundaryIndex; |
+ for (NSUInteger i = 0; i < numCardsToMove; i++) { |
+ DCHECK(currentIndex < numCards); |
+ CGFloat delta = startStack ? i * -maxStagger_ : i * maxStagger_; |
+ CGFloat newOrigin = startOffset + delta; |
+ [self moveOriginOfCardAtIndex:currentIndex toOffset:newOrigin]; |
+ currentIndex = startStack ? currentIndex - 1 : currentIndex + 1; |
+ } |
+} |
+ |
+- (CGFloat)distanceBetweenCardAtIndex:(NSUInteger)firstIndex |
+ andCardAtIndex:(NSUInteger)secondIndex { |
+ DCHECK(firstIndex < [cards_ count]); |
+ DCHECK(secondIndex < [cards_ count]); |
+ CGFloat firstOrigin = |
+ [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:firstIndex]]; |
+ CGFloat secondOrigin = |
+ [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:secondIndex]]; |
+ return std::abs(secondOrigin - firstOrigin); |
+} |
+ |
+- (BOOL)overextensionTowardStartOnCardAtIndex:(NSUInteger)index { |
+ DCHECK(index < [cards_ count]); |
+ CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]]; |
+ CGFloat collapsedOffset = |
+ startLimit_ + [self staggerOffsetForIndexFromEdge:index]; |
+ // Uses an epsilon to allow for floating-point imprecision. |
+ return (offset < collapsedOffset - kDistanceBeforeOverextension); |
+} |
+ |
+- (BOOL)overextensionTowardEndOnFirstCard { |
+ if ([cards_ count] == 0) |
+ return NO; |
+ CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]]; |
+ // Uses an epsilon to allow for floating-point imprecision. |
+ return (offset > startLimit_ + kDistanceBeforeOverextension); |
+} |
+ |
+- (CGFloat)limitOfOverextensionTowardStart { |
+ return startLimit_ - maximumOverextensionAmount_; |
+} |
+ |
+- (CGFloat)limitOfOverscrollTowardEnd { |
+ return startLimit_ + maximumOverextensionAmount_; |
+} |
+ |
+- (void)capOverscrollWithScrolledIndex:(NSUInteger)scrolledIndex |
+ allowEarlyOverscroll:(BOOL)allowEarlyOverscroll { |
+ DCHECK(scrolledIndex < [cards_ count]); |
+ [self capOverscrollTowardEnd]; |
+ // Allow for overscroll as appropriate when laying out the start stack. |
+ NSUInteger allowedStartOverscrollIndex = |
+ allowEarlyOverscroll ? scrolledIndex : [cards_ count] - 1; |
+ CGFloat startLimit = |
+ [self startStackLimitAllowingForOverextensionOnCardAtIndex: |
+ allowedStartOverscrollIndex]; |
+ [self layOutEdgeStacksWithStartLimit:startLimit]; |
+} |
+ |
+// Reduces overscroll on the first card to its maximum allowed amount, and |
+// undoes the effect of the extra overscroll on the rest of the cards. NOTE: In |
+// the current implementation of scroll, undoing the effect of the extra |
+// overscroll on the rest of the cards is as simple as moving them the reverse |
+// of the extra overscroll amount. If the implementation of scroll becomes more |
+// complex, undoing the effect of the extra overscroll may have to become more |
+// complex to correspond. |
+- (void)capOverscrollTowardEnd { |
+ if ([cards_ count] == 0) |
+ return; |
+ CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]]; |
+ CGFloat distance = firstCardOffset - [self limitOfOverscrollTowardEnd]; |
+ if (distance > 0) |
+ [self moveCardsFromIndex:0 toIndex:[cards_ count] - 1 byAmount:-distance]; |
+} |
+ |
+- (void)eliminateOverextension { |
+ if (treatOverExtensionAsScroll_) |
+ [self eliminateOverscroll]; |
+ else |
+ [self eliminateOverpinch]; |
+} |
+ |
+// If eliminating overscroll that was toward the end (where cards have |
+// overscrolled into the end stack), the cards scroll so that cards fan out |
+// from the end stack properly. If eliminating overscroll from the start, the |
+// overscrolled cards simply move back into place. |
+- (void)eliminateOverscroll { |
+ if ([cards_ count] == 0) |
+ return; |
+ [self setSynchronizeCardViews:NO]; |
+ CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]]; |
+ CGFloat overscrollEliminationAmount = startLimit_ - firstCardOffset; |
+ if (overscrollEliminationAmount <= 0) { |
+ [self scrollCardAtIndex:0 |
+ byDelta:overscrollEliminationAmount |
+ allowEarlyOverscroll:YES |
+ decayOnOverscroll:NO |
+ scrollLeadingCards:YES]; |
+ } |
+ [self layOutEdgeStacksWithStartLimit:startLimit_]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (void)eliminateOverpinch { |
+ if ([cards_ count] == 0) |
+ return; |
+ DCHECK(previousFirstPinchCardIndex_ != NSNotFound); |
+ DCHECK(previousSecondPinchCardIndex_ != NSNotFound); |
+ DCHECK(previousFirstPinchCardIndex_ < [cards_ count]); |
+ DCHECK(previousSecondPinchCardIndex_ < [cards_ count]); |
+ CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]]; |
+ CGFloat overpinchReductionAmount = startLimit_ - firstCardOffset; |
+ if (overpinchReductionAmount >= 0) { |
+ // Overpinching was toward the start stack. The overpinched cards simply |
+ // move back into place. |
+ [self layOutStartStackWithLimit:startLimit_]; |
+ } else { |
+ // Overpinching was toward the end stack. The effect of the overpinch is |
+ // undone by a corresponding negating pinch. |
+ [self handleMultitouchWithFirstDelta:overpinchReductionAmount |
+ secondDelta:0 |
+ firstCardIndex:previousFirstPinchCardIndex_ |
+ secondCardIndex:previousSecondPinchCardIndex_ |
+ decayOnOverpinch:NO]; |
+ } |
+ [self setSynchronizeCardViews:NO]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (void)scrollCardAtIndex:(NSUInteger)index |
+ byDelta:(CGFloat)delta |
+ allowEarlyOverscroll:(BOOL)allowEarlyOverscroll |
+ decayOnOverscroll:(BOOL)decayOnOverscroll |
+ scrollLeadingCards:(BOOL)scrollLeadingCards { |
+ NSUInteger numCards = [cards_ count]; |
+ if (numCards == 0) |
+ return; |
+ DCHECK(index < [cards_ count]); |
+ |
+ treatOverExtensionAsScroll_ = YES; |
+ |
+ // Temporarily turn off updates to the cards' views as this method might be |
+ // being called from within an animation, and updating the coordinates of a |
+ // |UIView| multiple times while it is animating can cause undesired |
+ // behavior. |
+ [self setSynchronizeCardViews:NO]; |
+ BOOL scrollIsTowardsEnd = (delta > 0); |
+ |
+ if (decayOnOverscroll) { |
+ // NOTE: This calculation is imprecise around the boundary case of a scroll |
+ // that moves the stack from not being overscrolled to being overscrolled. |
+ // This imprecision does not present a problem in practice, and eliminates |
+ // the need to compute the distance until the stack becomes overscrolled, |
+ // which is an unfortunately fiddly computation. |
+ if ([self overextensionTowardStartOnCardAtIndex:0] || |
+ [self overextensionTowardEndOnFirstCard]) |
+ delta = delta / kOverextensionDecayFactor; |
+ } |
+ |
+ NSUInteger leadingIndex = index; |
+ if (scrollLeadingCards) |
+ leadingIndex = scrollIsTowardsEnd ? numCards - 1 : 0; |
+ |
+ // Move the scrolled card and those further on in the direction being |
+ // scrolled by |delta|. |
+ if (scrollIsTowardsEnd) |
+ [self moveCardsFromIndex:index toIndex:leadingIndex byAmount:delta]; |
+ else |
+ [self moveCardsFromIndex:leadingIndex toIndex:index byAmount:delta]; |
+ |
+ // Move the cards trailing the scrolled card, but restore fan out in the |
+ // process as necessary. |
+ [self moveCardsrestoringFanoutFromIndex:index |
+ toEnd:!scrollIsTowardsEnd |
+ byAmount:delta |
+ restoreFanOutInStartStack:allowEarlyOverscroll]; |
+ |
+ [self capOverscrollWithScrolledIndex:index |
+ allowEarlyOverscroll:allowEarlyOverscroll]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (void)scrollCardAtIndex:(NSUInteger)index awayFromNeighbor:(BOOL)preceding { |
+ DCHECK(index < [cards_ count]); |
+ if (index == 0) |
+ return; |
+ |
+ CGFloat currentOffset = |
+ [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]]; |
+ CGFloat offsetToScrollTo = |
+ preceding ? currentOffset + kScrollAwayFromNeighborAmount |
+ : currentOffset - kScrollAwayFromNeighborAmount; |
+ |
+ CGFloat limitOffsetToScrollTo; |
+ if (index == [cards_ count] - 1 && !preceding) { |
+ limitOffsetToScrollTo = endLimit_ - [self maximumCardSeparation]; |
+ } else { |
+ CGFloat neighborOffset = |
+ preceding |
+ ? [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index - 1]] |
+ : [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index + 1]]; |
+ limitOffsetToScrollTo = preceding |
+ ? neighborOffset + [self maximumCardSeparation] |
+ : neighborOffset - [self maximumCardSeparation]; |
+ } |
+ offsetToScrollTo = preceding |
+ ? std::min(offsetToScrollTo, limitOffsetToScrollTo) |
+ : std::max(offsetToScrollTo, limitOffsetToScrollTo); |
+ |
+ CGFloat distanceToScroll = offsetToScrollTo - currentOffset; |
+ |
+ [self setSynchronizeCardViews:NO]; |
+ if (preceding) { |
+ [self moveCardsFromIndex:index |
+ toIndex:[cards_ count] - 1 |
+ byAmount:distanceToScroll]; |
+ } else { |
+ [self moveCardsFromIndex:0 toIndex:index byAmount:distanceToScroll]; |
+ } |
+ [self layOutEdgeStacksWithStartLimit:startLimit_]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (void)moveCardsrestoringFanoutFromIndex:(NSUInteger)index |
+ toEnd:(BOOL)toEnd |
+ byAmount:(CGFloat)amount |
+ restoreFanOutInStartStack:(BOOL)restoreFanOutInStartStack { |
+ DCHECK(index < [cards_ count]); |
+ |
+ // This method assumes that the cards are being moved toward the card at |
+ // |index|. |
+ if (toEnd) |
+ DCHECK(amount <= 0); |
+ else |
+ DCHECK(amount >= 0); |
+ |
+ CGFloat currentAmount = amount; |
+ // The index of the card against which separation will be checked for the |
+ // card currently being moved. |
+ NSUInteger precedingIndex = index; |
+ // The index of the card currently being moved. |
+ NSUInteger currentIndex = toEnd ? precedingIndex + 1 : precedingIndex - 1; |
+ NSInteger step = toEnd ? 1 : -1; |
+ |
+ // Move all the cards after/before the one at |index| as indicated by |toEnd|. |
+ NSInteger numCardsToMove = toEnd ? ([cards_ count] - index - 1) : index; |
+ for (int i = 0; i < numCardsToMove; i++) { |
+ BOOL restoreFanout = YES; |
+ // Do not restore fanout when cards are moving into an edge stack unless |
+ // directed to. |
+ if (toEnd) { |
+ if (!restoreFanOutInStartStack) { |
+ restoreFanout = (![self isInStartStack:currentIndex] && |
+ ![self isInStartStack:precedingIndex]); |
+ } |
+ } else { |
+ restoreFanout = (![self isInEndStack:currentIndex] && |
+ ![self isInEndStack:precedingIndex]); |
+ } |
+ |
+ if (restoreFanout) { |
+ CGFloat distance = [self distanceBetweenCardAtIndex:currentIndex |
+ andCardAtIndex:precedingIndex]; |
+ // Account for the fact that the card at |precedingIndex| has already |
+ // been moved. |
+ distance -= std::abs(currentAmount); |
+ // Calculate how much of the move (if any) should be eliminated in order |
+ // to restore fan out between this card and the preceding card. |
+ CGFloat amountToRestoreFanOut = |
+ std::max((CGFloat)0, maxStagger_ - distance); |
+ if (amountToRestoreFanOut > std::abs(currentAmount)) |
+ currentAmount = 0; |
+ else if (currentAmount > 0) |
+ currentAmount -= amountToRestoreFanOut; |
+ else |
+ currentAmount += amountToRestoreFanOut; |
+ } |
+ [self moveCardAtIndex:currentIndex byAmount:currentAmount]; |
+ precedingIndex = currentIndex; |
+ currentIndex += step; |
+ } |
+} |
+ |
+- (CGFloat)clipDelta:(CGFloat)delta forCardAtIndex:(NSInteger)index { |
+ DCHECK(index < (NSInteger)[cards_ count]); |
+ StackCard* card = [cards_ objectAtIndex:index]; |
+ CGFloat startingOffset = [self cardOffsetOnLayoutAxis:card]; |
+ if (delta < 0) { |
+ // |delta| is towards start stack. |
+ CGFloat collapsedPosition = |
+ startLimit_ + [self staggerOffsetForIndexFromEdge:index]; |
+ delta = std::max(delta, collapsedPosition - startingOffset); |
+ } else { |
+ // |delta| is towards end stack. |
+ NSInteger indexFromEnd = [cards_ count] - 1 - index; |
+ CGFloat collapsedPosition = |
+ endLimit_ - kMinStackStaggerAmount - |
+ [self staggerOffsetForIndexFromEdge:indexFromEnd]; |
+ delta = std::min(delta, collapsedPosition - startingOffset); |
+ } |
+ return delta; |
+} |
+ |
+- (CGFloat)maximumCardSeparation { |
+ return [self layoutLength:self.cardSize] - kFullyExtendedCardOverlap; |
+} |
+ |
+- (CGFloat)maximumOffsetForCardAtIndex:(NSInteger)index { |
+ DCHECK(index < (NSInteger)[cards_ count]); |
+ // Account for the fact that the first card may be overextended toward the |
+ // start or the end. |
+ CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]]; |
+ return firstCardOffset + index * [self maximumCardSeparation]; |
+} |
+ |
+- (CGFloat)cappedFanoutOffsetForCardAtIndex:(NSInteger)index { |
+ CGFloat fannedOutPosition = startLimit_ + index * maxStagger_; |
+ NSInteger indexFromEnd = [cards_ count] - 1 - index; |
+ CGFloat endStackPosition = endLimit_ - kMinStackStaggerAmount - |
+ [self staggerOffsetForIndexFromEdge:indexFromEnd]; |
+ return std::min(fannedOutPosition, endStackPosition); |
+} |
+ |
+- (void)moveCardAtIndex:(NSUInteger)index byAmount:(CGFloat)amount { |
+ DCHECK(index < [cards_ count]); |
+ [self moveCard:cards_[index] byAmount:amount]; |
+} |
+ |
+- (void)moveCard:(StackCard*)card byAmount:(CGFloat)amount { |
+ DCHECK(card); |
+ LayoutRect layout = card.layout; |
+ if (layoutIsVertical_) { |
+ layout.position.leading = layoutAxisPosition_ - 0.5 * card.size.width; |
+ layout.position.originY += amount; |
+ } else { |
+ layout.position.leading += amount; |
+ layout.position.originY = layoutAxisPosition_ - 0.5 * card.size.height; |
+ } |
+ card.layout = layout; |
+} |
+ |
+- (void)moveCardsFromIndex:(NSUInteger)startIndex |
+ toIndex:(NSUInteger)endIndex |
+ byAmount:(CGFloat)amount { |
+ DCHECK(startIndex <= endIndex); |
+ DCHECK(endIndex < [cards_ count]); |
+ for (NSUInteger i = startIndex; i <= endIndex; ++i) { |
+ [self moveCardAtIndex:i byAmount:amount]; |
+ } |
+} |
+ |
+- (void)moveOriginOfCardAtIndex:(NSUInteger)index toOffset:(CGFloat)offset { |
+ DCHECK(index < [cards_ count]); |
+ StackCard* card = [cards_ objectAtIndex:index]; |
+ CGFloat startingOffset = [self cardOffsetOnLayoutAxis:card]; |
+ [self moveCard:card byAmount:offset - startingOffset]; |
+} |
+ |
+// Constrains offset to satisfy the following constraints: |
+// - >= |kMinStackStaggerAmount| away from origin of constraining neighbor. |
+// - <= |maximumCardSeparation:| away from origin of constraining neighbor. |
+// - <= |maximumOffsetForCardAtIndex:index|. |
+- (CGFloat)constrainedOffset:(CGFloat)offset |
+ forCardAtIndex:(NSInteger)index |
+ constrainingNeighborIsPrevious:(BOOL)isPrevious { |
+ DCHECK(index < (NSInteger)[cards_ count]); |
+ if (isPrevious) |
+ DCHECK(index > 0); |
+ else |
+ DCHECK(index < (NSInteger)[cards_ count] - 1); |
+ |
+ CGFloat constrainingIndex = isPrevious ? index - 1 : index + 1; |
+ StackCard* constrainingCard = [cards_ objectAtIndex:constrainingIndex]; |
+ CGFloat constrainingCardOffset = |
+ [self cardOffsetOnLayoutAxis:constrainingCard]; |
+ // Ensures that the above constraints are mutually satisfiable. |
+ DCHECK(constrainingCardOffset <= |
+ [self maximumOffsetForCardAtIndex:constrainingIndex]); |
+ |
+ CGFloat minOffset, maxOffset; |
+ if (isPrevious) { |
+ minOffset = constrainingCardOffset + kMinStackStaggerAmount; |
+ maxOffset = constrainingCardOffset + [self maximumCardSeparation]; |
+ maxOffset = std::min(maxOffset, [self maximumOffsetForCardAtIndex:index]); |
+ } else { |
+ minOffset = constrainingCardOffset - [self maximumCardSeparation]; |
+ maxOffset = constrainingCardOffset - kMinStackStaggerAmount; |
+ maxOffset = std::min(maxOffset, [self maximumOffsetForCardAtIndex:index]); |
+ } |
+ DCHECK(minOffset <= maxOffset); |
+ offset = std::max(offset, minOffset); |
+ offset = std::min(offset, maxOffset); |
+ return offset; |
+} |
+ |
+// If |towardsEnd|, then all cards up to and including the last card are moved, |
+// with each card being constrained by the position of its previous neighbor. |
+// Otherwise, all cards down to but *not* including the first card are moved, |
+// with each card being constrained by the position of its following neighbor. |
+// NOTE: It is assumed that at the time of calling this method that the |
+// boundary card for the movement (i.e., the card before |index| if |
+// |towardsEnd|, the card after |index| otherwise), if it exists, is in its |
+// desired position, as constraining is performed in this method with respect |
+// to the position of that boundary card. |
+- (void)moveCardsStartingAtIndex:(NSInteger)index |
+ towardsEnd:(BOOL)towardsEnd |
+ withDrivingDelta:(CGFloat)drivingDelta { |
+ const CGFloat kDecayFactor = 2.0; |
+ DCHECK(index < (NSInteger)[cards_ count]); |
+ DCHECK(index >= 0); |
+ |
+ NSInteger numCardsToMove; |
+ if (towardsEnd) |
+ numCardsToMove = [cards_ count] - index; |
+ else |
+ numCardsToMove = index; |
+ |
+ NSInteger currentIndex = index; |
+ CGFloat currentDelta = drivingDelta / kDecayFactor; |
+ for (int i = 0; i < numCardsToMove; i++) { |
+ StackCard* card = [cards_ objectAtIndex:currentIndex]; |
+ CGFloat cardStartingOffset = [self cardOffsetOnLayoutAxis:card]; |
+ CGFloat cardEndingOffset = |
+ [self constrainedOffset:cardStartingOffset + currentDelta |
+ forCardAtIndex:currentIndex |
+ constrainingNeighborIsPrevious:towardsEnd]; |
+ [self moveOriginOfCardAtIndex:currentIndex toOffset:cardEndingOffset]; |
+ |
+ currentIndex = towardsEnd ? currentIndex + 1 : currentIndex - 1; |
+ currentDelta = (cardEndingOffset - cardStartingOffset) / kDecayFactor; |
+ } |
+} |
+ |
+// Moves cards as follows: |
+// - the card at |firstIndex| moves by |firstDelta|. |
+// - the card at |secondIndex| moves by |secondDelta|. |
+// - the cards in-between move by a combination of |firstDelta| and |
+// |secondDelta|, with the contribution of each being weighted by the |
+// closeness of the card's starting position to the starting positions of the |
+// cards at |firstIndex| and |secondIndex| respectively. |
+// Each card is constrained to be within its maximum offset, and each card |
+// other than the first is constrained by the position of its previous |
+// neighbor. |
+// NOTE: It is assumed that at the time of calling this method the card before |
+// |firstIndex| and the card after |secondIndex|, if they exist, are not |
+// necessarily in their desired positions. Hence, no constraining is performed |
+// in this method with respect to the positions of those boundary cards. |
+- (void)blendOffsetsOfCardsBetweenFirstIndex:(NSInteger)firstIndex |
+ secondIndex:(NSInteger)secondIndex |
+ withFirstDelta:(CGFloat)firstDelta |
+ secondDelta:(CGFloat)secondDelta { |
+ DCHECK(firstIndex < secondIndex); |
+ DCHECK(secondIndex < (NSInteger)[cards_ count]); |
+ StackCard* firstCard = [cards_ objectAtIndex:firstIndex]; |
+ CGFloat firstStartingOffset = [self cardOffsetOnLayoutAxis:firstCard]; |
+ StackCard* secondCard = [cards_ objectAtIndex:secondIndex]; |
+ CGFloat secondStartingOffset = [self cardOffsetOnLayoutAxis:secondCard]; |
+ CGFloat firstEndingOffset = firstStartingOffset + firstDelta; |
+ CGFloat secondEndingOffset = secondStartingOffset + secondDelta; |
+ |
+ // Move each card by a combination of |firstDelta| and |secondDelta|, with |
+ // the contribution of each being weighted by the card's closeness |
+ // to |firstStartingOffset| and |secondStartingOffset| respectively. |
+ for (NSInteger i = firstIndex; i <= secondIndex; i++) { |
+ StackCard* card = [cards_ objectAtIndex:i]; |
+ CGFloat cardStartingOffset = [self cardOffsetOnLayoutAxis:card]; |
+ CGFloat weightOfSecondDelta = (cardStartingOffset - firstStartingOffset) / |
+ (secondStartingOffset - firstStartingOffset); |
+ CGFloat weightOfFirstDelta = 1 - weightOfSecondDelta; |
+ CGFloat cardEndingOffset = weightOfFirstDelta * firstEndingOffset + |
+ weightOfSecondDelta * secondEndingOffset; |
+ // First card being moved is not constrained to previous neighbor but is |
+ // constrained to be within its maximum offset unless it is the first card |
+ // of the deck, which is allowed to move off its maximum offset for an |
+ // overpinch effect. |
+ if (i == firstIndex) { |
+ if (i > 0) { |
+ cardEndingOffset = std::min( |
+ cardEndingOffset, [self maximumOffsetForCardAtIndex:firstIndex]); |
+ } |
+ } else { |
+ cardEndingOffset = [self constrainedOffset:cardEndingOffset |
+ forCardAtIndex:i |
+ constrainingNeighborIsPrevious:YES]; |
+ } |
+ [self moveOriginOfCardAtIndex:i toOffset:cardEndingOffset]; |
+ } |
+} |
+ |
+// - The cards at indices between |firstCardIndex| and |secondCardIndex| |
+// inclusive are blended proportionally between the ending positions of those |
+// two cards. |
+// - The cards at indices < |firstCardIndex| are adjusted based on |firstDelta| |
+// with an exponential decay. |
+// - The cards at indices > |secondCardIndex| are adjusted based on |
+// |secondDelta| with an exponential decay. |
+- (void)handleMultitouchWithFirstDelta:(CGFloat)firstDelta |
+ secondDelta:(CGFloat)secondDelta |
+ firstCardIndex:(NSInteger)firstCardIndex |
+ secondCardIndex:(NSInteger)secondCardIndex |
+ decayOnOverpinch:(BOOL)decayOnOverpinch { |
+ DCHECK(firstCardIndex < secondCardIndex); |
+ NSInteger numCards = (NSInteger)[cards_ count]; |
+ DCHECK(secondCardIndex < numCards); |
+ |
+ treatOverExtensionAsScroll_ = NO; |
+ previousFirstPinchCardIndex_ = firstCardIndex; |
+ previousSecondPinchCardIndex_ = secondCardIndex; |
+ |
+ // Temporarily turn off updates to the cards' views as this method might be |
+ // being called from within an animation, and updating the coordinates of a |
+ // |UIView| multiple times while it is animating can cause undesired |
+ // behavior. |
+ [self setSynchronizeCardViews:NO]; |
+ |
+ if (decayOnOverpinch) { |
+ if ([self overextensionTowardStartOnCardAtIndex:firstCardIndex] || |
+ (firstCardIndex == 0 && [self overextensionTowardEndOnFirstCard])) |
+ firstDelta /= kOverextensionDecayFactor; |
+ if ([self overextensionTowardStartOnCardAtIndex:secondCardIndex] || |
+ (secondCardIndex == 0 && [self overextensionTowardEndOnFirstCard])) |
+ secondDelta /= kOverextensionDecayFactor; |
+ } |
+ |
+ // Blend the positions of the cards between the two touched cards (inclusive). |
+ // This step must be performed first, as the following two calls assume that |
+ // |firstCardIndex| and |secondCardIndex| are in their correct positions when |
+ // calculating constraints for positions of other cards. |
+ [self blendOffsetsOfCardsBetweenFirstIndex:firstCardIndex |
+ secondIndex:secondCardIndex |
+ withFirstDelta:firstDelta |
+ secondDelta:secondDelta]; |
+ |
+ // Adjust the cards after |secondCardIndex| and before |firstCardIndex|. |
+ if (secondCardIndex < numCards - 1) { |
+ [self moveCardsStartingAtIndex:secondCardIndex + 1 |
+ towardsEnd:YES |
+ withDrivingDelta:secondDelta]; |
+ } |
+ if (firstCardIndex > 0) { |
+ [self moveCardsStartingAtIndex:firstCardIndex - 1 |
+ towardsEnd:NO |
+ withDrivingDelta:firstDelta]; |
+ } |
+ |
+ // Perform start and end capping, allowing overextension on the start stack as |
+ // determined by the offset of the first pinched card. |
+ CGFloat startLimit = [self |
+ startStackLimitAllowingForOverextensionOnCardAtIndex:firstCardIndex]; |
+ [self layOutEdgeStacksWithStartLimit:startLimit]; |
+ [self setSynchronizeCardViews:YES]; |
+} |
+ |
+- (CGFloat)startStackLimitAllowingForOverextensionOnCardAtIndex: |
+ (NSUInteger)index { |
+ DCHECK(index < [cards_ count]); |
+ if (![self overextensionTowardStartOnCardAtIndex:index]) |
+ return startLimit_; |
+ // Calculate the start limit that will lay the start stack into place around |
+ // the card at |index|. |
+ CGFloat startLimit = |
+ [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]] - |
+ [self staggerOffsetForIndexFromEdge:index]; |
+ return std::max(startLimit, [self limitOfOverextensionTowardStart]); |
+} |
+ |
+- (void)layOutEdgeStacksWithStartLimit:(CGFloat)startLimit { |
+ [self layOutStartStackWithLimit:startLimit]; |
+ [self layOutEndStack]; |
+} |
+ |
+- (void)layOutStartStack { |
+ [self layOutStartStackWithLimit:startLimit_]; |
+} |
+ |
+- (void)layOutStartStackWithLimit:(CGFloat)limit { |
+ lastStartStackCardIndex_ = |
+ [self computeEdgeStackBoundaryIndex:YES withVisualStackLimit:limit]; |
+ if (lastStartStackCardIndex_ == -1) |
+ return; |
+ |
+ // Position the cards. Cards up to the last card of the start stack are |
+ // staggered backwards from the start stack's inner edge. |
+ CGFloat stackInnerEdge = |
+ [self computeEdgeStackInnerEdge:YES withVisualStackLimit:limit]; |
+ for (NSInteger i = 0; i <= lastStartStackCardIndex_; i++) { |
+ CGFloat distanceFromInnerEdge = |
+ (lastStartStackCardIndex_ - i) * kMinStackStaggerAmount; |
+ CGFloat offset = std::max(limit, stackInnerEdge - distanceFromInnerEdge); |
+ [self moveOriginOfCardAtIndex:i toOffset:offset]; |
+ } |
+} |
+ |
+- (void)layOutEndStack { |
+ NSInteger numCards = [cards_ count]; |
+ // When laying out the stack, leave enough room so that the last card is |
+ // visible. |
+ CGFloat visualLimit = endLimit_ - kMinStackStaggerAmount; |
+ firstEndStackCardIndex_ = |
+ [self computeEdgeStackBoundaryIndex:NO withVisualStackLimit:visualLimit]; |
+ if (firstEndStackCardIndex_ == numCards) |
+ return; |
+ |
+ // Position the cards. Cards from the first card of the end stack are |
+ // staggered forwards from the end stack's inner edge. |
+ CGFloat stackInnerEdge = |
+ [self computeEdgeStackInnerEdge:NO withVisualStackLimit:visualLimit]; |
+ for (NSInteger i = firstEndStackCardIndex_; i < numCards; i++) { |
+ CGFloat distanceFromInnerEdge = |
+ (i - firstEndStackCardIndex_) * kMinStackStaggerAmount; |
+ CGFloat offset = |
+ std::min(visualLimit, stackInnerEdge + distanceFromInnerEdge); |
+ [self moveOriginOfCardAtIndex:i toOffset:offset]; |
+ } |
+} |
+ |
+- (NSInteger)computeEdgeStackBoundaryIndex:(BOOL)startStack |
+ withVisualStackLimit:(CGFloat)visualStackLimit { |
+ NSInteger numCards = [cards_ count]; |
+ NSInteger boundaryIndex = startStack ? -1 : numCards; |
+ for (NSInteger i = 0; i < numCards; ++i) { |
+ StackCard* card = [cards_ objectAtIndex:i]; |
+ CGFloat uncappedPosition = [self cardOffsetOnLayoutAxis:card]; |
+ if (startStack) { |
+ CGFloat pushThreshold = |
+ visualStackLimit + [self pushThresholdForIndexFromEdge:i]; |
+ if (uncappedPosition <= pushThreshold) |
+ boundaryIndex = i; |
+ } else { |
+ NSInteger indexFromEnd = numCards - 1 - i; |
+ CGFloat pushThreshold = |
+ visualStackLimit - [self pushThresholdForIndexFromEdge:indexFromEnd]; |
+ if (uncappedPosition >= pushThreshold) { |
+ boundaryIndex = i; |
+ break; |
+ } |
+ } |
+ } |
+ return boundaryIndex; |
+} |
+ |
+- (CGFloat)computeEdgeStackInnerEdge:(BOOL)startStack |
+ withVisualStackLimit:(CGFloat)visualStackLimit { |
+ NSInteger boundaryIndex = |
+ startStack ? lastStartStackCardIndex_ : firstEndStackCardIndex_; |
+ DCHECK(boundaryIndex >= 0); |
+ DCHECK(boundaryIndex < (NSInteger)[cards_ count]); |
+ StackCard* card = [cards_ objectAtIndex:boundaryIndex]; |
+ CGFloat offset = [self cardOffsetOnLayoutAxis:card]; |
+ NSUInteger indexFromEnd = [cards_ count] - 1 - boundaryIndex; |
+ CGFloat cap = startStack |
+ ? visualStackLimit + |
+ [self staggerOffsetForIndexFromEdge:boundaryIndex] |
+ : visualStackLimit - |
+ [self staggerOffsetForIndexFromEdge:indexFromEnd]; |
+ return startStack ? std::max(cap, offset) : std::min(cap, offset); |
+} |
+ |
+- (CGFloat)fannedStackLength { |
+ if ([cards_ count] == 0) |
+ return 0; |
+ CGFloat cardLength = [self layoutLength:cardSize_]; |
+ return maxStagger_ * ([cards_ count] - 1) + cardLength; |
+} |
+ |
+- (CGFloat)maximumStackLength { |
+ if ([cards_ count] == 0) |
+ return 0; |
+ CGFloat cardLength = [self layoutLength:cardSize_]; |
+ return [self maximumCardSeparation] * ([cards_ count] - 1) + cardLength; |
+} |
+ |
+- (CGFloat)fullyCollapsedStackLength { |
+ CGFloat staggerLength = |
+ kMinStackStaggerAmount * (kMaxVisibleStaggerCount - 1); |
+ return [self layoutLength:cardSize_] + staggerLength; |
+} |
+ |
+- (CGFloat)layoutLength:(CGSize)size { |
+ return layoutIsVertical_ ? size.height : size.width; |
+} |
+ |
+- (CGFloat)layoutOffset:(LayoutRectPosition)position { |
+ return layoutIsVertical_ ? position.originY : position.leading; |
+} |
+ |
+- (CGFloat)cardOffsetOnLayoutAxis:(StackCard*)card { |
+ return [self layoutOffset:card.layout.position]; |
+} |
+ |
+- (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge { |
+ return std::min(countFromEdge, kMaxVisibleStaggerCount - 1) * |
+ kMinStackStaggerAmount; |
+} |
+ |
+- (CGFloat)pushThresholdForIndexFromEdge:(NSInteger)countFromEdge { |
+ return std::min(countFromEdge, kMaxVisibleStaggerCount) * |
+ kMinStackStaggerAmount; |
+} |
+ |
+- (BOOL)cardIsCovered:(StackCard*)card { |
+ NSUInteger index = [cards_ indexOfObject:card]; |
+ DCHECK(index != NSNotFound); |
+ DCHECK(index < [cards_ count]); |
+ |
+ if (index == [cards_ count] - 1) |
+ return NO; |
+ |
+ // Card positions are non-decreasing, and cards are all the same size, so a |
+ // card is completely covered iff the next card is in exactly the same |
+ // position (in terms of screen coordinates). |
+ StackCard* nextCard = [cards_ objectAtIndex:(index + 1)]; |
+ LayoutRectPosition position = |
+ AlignLayoutRectPositionToPixel(card.layout.position); |
+ LayoutRectPosition nextPosition = |
+ AlignLayoutRectPositionToPixel(nextCard.layout.position); |
+ return LayoutRectPositionEqualToPosition(position, nextPosition); |
+} |
+ |
+- (BOOL)cardIsCollapsed:(StackCard*)card { |
+ NSUInteger index = [cards_ indexOfObject:card]; |
+ DCHECK(index != NSNotFound); |
+ DCHECK(index < [cards_ count]); |
+ |
+ // Last card is collapsed if close enough to edge that title isn't visible. |
+ if (index == [cards_ count] - 1) { |
+ CGFloat cardOffset = [self cardOffsetOnLayoutAxis:card]; |
+ CGFloat edgeOffset = endLimit_ - kMinStackStaggerAmount; |
+ return cardOffset >= edgeOffset; |
+ } |
+ CGFloat separation = |
+ [self distanceBetweenCardAtIndex:index andCardAtIndex:(index + 1)]; |
+ return separation <= kMinStackStaggerAmount; |
+} |
+ |
+- (BOOL)cardLabelCovered:(StackCard*)card { |
+ NSUInteger index = [cards_ indexOfObject:card]; |
+ CGFloat labelOffset = [card.view titleLabel].frame.size.height; |
+ if (index == [cards_ count] - 1) { |
+ CGFloat cardOffset = [self cardOffsetOnLayoutAxis:card]; |
+ CGFloat edgeOffset = endLimit_ - labelOffset; |
+ return cardOffset >= edgeOffset; |
+ } else { |
+ CGFloat separation = |
+ [self distanceBetweenCardAtIndex:index andCardAtIndex:(index + 1)]; |
+ return separation <= labelOffset; |
+ } |
+} |
+ |
+- (void)setSynchronizeCardViews:(BOOL)synchronizeViews { |
+ for (StackCard* card in cards_.get()) { |
+ card.synchronizeView = synchronizeViews; |
+ } |
+} |
+ |
+- (BOOL)isInStartStack:(NSUInteger)index { |
+ DCHECK(index < [cards_ count]); |
+ return ((NSInteger)index <= lastStartStackCardIndex_); |
+} |
+ |
+- (BOOL)isInEndStack:(NSUInteger)index { |
+ DCHECK(index < [cards_ count]); |
+ return ((NSInteger)index >= firstEndStackCardIndex_); |
+} |
+ |
+- (BOOL)isInEdgeStack:(NSUInteger)index { |
+ return ([self isInStartStack:index] || [self isInEndStack:index]); |
+} |
+ |
+- (BOOL)stackIsFullyCollapsed { |
+ NSInteger numCards = [cards_ count]; |
+ if (numCards == 0) |
+ return YES; |
+ return (lastStartStackCardIndex_ == (numCards - 1)); |
+} |
+ |
+- (BOOL)stackIsFullyFannedOut { |
+ for (NSUInteger i = 0; i < [cards_ count]; i++) { |
+ CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:i]]; |
+ if (offset < [self cappedFanoutOffsetForCardAtIndex:i]) |
+ return NO; |
+ } |
+ return YES; |
+} |
+ |
+- (BOOL)stackIsFullyOverextended { |
+ NSInteger numCards = [cards_ count]; |
+ if (numCards == 0) |
+ return YES; |
+ |
+ // Test for being fully overextended toward the start. |
+ StackCard* lastCard = [cards_ objectAtIndex:numCards - 1]; |
+ CGFloat lastCardOrigin = [self cardOffsetOnLayoutAxis:lastCard]; |
+ // Note that -limitOfOverextensionTowardStart is defined with respect to the |
+ // *start* of the stack. |
+ if ((lastCardOrigin - [self staggerOffsetForIndexFromEdge:numCards - 1]) <= |
+ [self limitOfOverextensionTowardStart]) |
+ return YES; |
+ |
+ // Test for being fully overextended toward the end. |
+ StackCard* firstCard = [cards_ firstObject]; |
+ return ([self cardOffsetOnLayoutAxis:firstCard] >= |
+ [self limitOfOverscrollTowardEnd]); |
+} |
+ |
+- (CGFloat)overextensionAmount { |
+ if ([cards_ count] == 0) |
+ return 0; |
+ return std::abs([self cardOffsetOnLayoutAxis:[cards_ firstObject]] - |
+ startLimit_); |
+} |
+ |
+- (NSUInteger)fannedStackCount { |
+ return floor((endLimit_ - startLimit_) / maxStagger_); |
+} |
+ |
+@end |