blob: decc4986f08a2fefc75c69a46f1cadeb053e5001 [file] [log] [blame]
// 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/card_view.h"
#import "ios/chrome/browser/ui/stack_view/stack_card.h"
#import "ios/chrome/browser/ui/stack_view/title_label.h"
#import "ios/chrome/browser/ui/ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
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 () {
NSMutableArray* cards_;
// YES if the previous call to one of {|scrollCardAtIndex|,
// |handleMultitouchWithFirstDelta|} was to the former method; NO otherwise.
BOOL treatOverExtensionAsScroll_;
NSUInteger previousFirstPinchCardIndex_;
NSUInteger previousSecondPinchCardIndex_;
}
// 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_ = [[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_) {
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_) {
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_) {
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