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

Unified Diff: ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: ios/chrome/browser/ui/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

Powered by Google App Engine
This is Rietveld 408576698