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

Unified Diff: ios/chrome/browser/ui/stack_view/card_stack_layout_manager_unittest.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_unittest.mm
diff --git a/ios/chrome/browser/ui/stack_view/card_stack_layout_manager_unittest.mm b/ios/chrome/browser/ui/stack_view/card_stack_layout_manager_unittest.mm
new file mode 100644
index 0000000000000000000000000000000000000000..302780a3f9905d96260364de3fce0836f7556e5f
--- /dev/null
+++ b/ios/chrome/browser/ui/stack_view/card_stack_layout_manager_unittest.mm
@@ -0,0 +1,1725 @@
+// 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.
+
+#include "base/mac/scoped_nsobject.h"
+#include "ios/chrome/browser/ui/rtl_geometry.h"
+#import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h"
+#import "ios/chrome/browser/ui/stack_view/stack_card.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "testing/platform_test.h"
+
+@interface CardStackLayoutManager (Private)
+- (CGFloat)minStackStaggerAmount;
+- (CGFloat)scrollCardAwayFromNeighborAmount;
+- (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge;
+- (CGFloat)maximumCardSeparation;
+@end
+
+// A mock version of StackCard.
+@interface MockStackCard : NSObject
+
+@property(nonatomic, readwrite, assign) BOOL synchronizeView;
+@property(nonatomic, readwrite, assign) LayoutRect layout;
+@property(nonatomic, readwrite, assign) CGSize size;
+
+@end
+
+@implementation MockStackCard
+
+@synthesize synchronizeView = _synchronizeView;
+@synthesize layout = _layout;
+@synthesize size = _size;
+
+- (void)setSize:(CGSize)size {
+ _layout.position.leading += (_layout.size.width - size.width) / 2.0;
+ _layout.position.originY += (_layout.size.height - size.height) / 2.0;
+ _layout.size = size;
+ _size = size;
+}
+
+@end
+
+namespace {
+
+// Create a fixture to get an autorelease pool.
+class CardStackLayoutManagerTest : public PlatformTest {};
+
+const float kMargin = 5;
+const float kMaxStagger = 40;
+const float kAxisPosition = 55;
+const float kCardWidth = 300;
+const float kCardHeight = 400;
+const float kDefaultEndLimitFraction = 0.4;
+
+// Returns the offset of |point| in the current layout direction.
+CGFloat LayoutOffset(CardStackLayoutManager* stack,
+ LayoutRectPosition position) {
+ return [stack layoutIsVertical] ? position.originY : position.leading;
+}
+
+// Returns the distance along the layout axis between the cards at |firstIndex|
+// and |secondIndex|.
+CGFloat SeparationOnLayoutAxis(CardStackLayoutManager* stack,
+ NSUInteger firstIndex,
+ NSUInteger secondIndex) {
+ StackCard* firstCard = [[stack cards] objectAtIndex:firstIndex];
+ StackCard* secondCard = [[stack cards] objectAtIndex:secondIndex];
+ CGFloat firstCardOffset = LayoutOffset(stack, firstCard.layout.position);
+ CGFloat secondCardOffset = LayoutOffset(stack, secondCard.layout.position);
+ return secondCardOffset - firstCardOffset;
+}
+
+// Validates basic constraints:
+// - All cards should be centered at kAxisPosition along the non-layout axis.
+// - If not overscrolled toward start, all start edges should be at or past
+// kMargin.
+// - All start edges should be visibly before endLimit.
+// - No card should start before a previous card.
+// - No card should start after the end of a previous card.
+// If |shouldBeWithinMaxStagger|:
+// - Consecutive cards should be no more than kMaxStagger apart.
+void ValidateCardPositioningConstraints(CardStackLayoutManager* stack,
+ CGFloat endLimit,
+ bool shouldBeWithinMaxStagger) {
+ BOOL isVertical = [stack layoutIsVertical];
+ StackCard* previousCard = nil;
+ for (StackCard* card in [stack cards]) {
+ CGRect cardFrame = LayoutRectGetRect(card.layout);
+ CGFloat nonLayoutAxisCenter =
+ isVertical ? CGRectGetMidX(cardFrame) : CGRectGetMidY(cardFrame);
+ EXPECT_FLOAT_EQ(kAxisPosition, nonLayoutAxisCenter);
+ CGFloat startEdge = LayoutOffset(stack, card.layout.position);
+ if (![stack overextensionTowardStartOnCardAtIndex:0])
+ EXPECT_LE(kMargin, startEdge);
+ EXPECT_GT(endLimit, startEdge);
+ if (previousCard != nil) {
+ CGFloat previousTopEdge =
+ LayoutOffset(stack, previousCard.layout.position);
+ EXPECT_LE(previousTopEdge, startEdge);
+ CGFloat cardSize = isVertical ? kCardHeight : kCardWidth;
+ EXPECT_GE(cardSize, startEdge - previousTopEdge);
+ if (shouldBeWithinMaxStagger)
+ EXPECT_GE(kMaxStagger, startEdge - previousTopEdge);
+ }
+ previousCard = card;
+ }
+}
+
+// Creates a new |CardStackLayoutManager|, adds |n| cards to it, and sets
+// dimensional/positioning parameters to the constants defined above.
+CardStackLayoutManager* newStackOfNCards(unsigned int n, BOOL layoutIsVertical)
+ NS_RETURNS_RETAINED {
+ CardStackLayoutManager* stack = [[CardStackLayoutManager alloc] init];
+ stack.layoutIsVertical = layoutIsVertical;
+ for (unsigned int i = 0; i < n; ++i) {
+ base::scoped_nsobject<StackCard> card(
+ (StackCard*)[[MockStackCard alloc] init]);
+ [stack addCard:card];
+ }
+
+ CGSize cardSize = CGSizeMake(kCardWidth, kCardHeight);
+ [stack setCardSize:cardSize];
+ [stack setStartLimit:kMargin];
+ [stack setMaxStagger:kMaxStagger];
+ [stack setLayoutAxisPosition:kAxisPosition];
+ CGFloat cardLength = layoutIsVertical ? cardSize.height : cardSize.width;
+ [stack setMaximumOverextensionAmount:cardLength / 2.0];
+
+ return stack;
+}
+
+TEST_F(CardStackLayoutManagerTest, CardSizing) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ [[CardStackLayoutManager alloc] init]);
+ stack.get().layoutIsVertical = boolValues[i];
+
+ base::scoped_nsobject<StackCard> view1(
+ (StackCard*)[[MockStackCard alloc] init]);
+ base::scoped_nsobject<StackCard> view2(
+ (StackCard*)[[MockStackCard alloc] init]);
+ base::scoped_nsobject<StackCard> view3(
+ (StackCard*)[[MockStackCard alloc] init]);
+ [stack addCard:view1.get()];
+ [stack addCard:view2.get()];
+ [stack addCard:view3.get()];
+ // Ensure that removed cards are not altered.
+ [stack removeCard:view2];
+
+ CGSize cardSize = CGSizeMake(111, 222);
+ [stack setCardSize:cardSize];
+
+ EXPECT_FLOAT_EQ(cardSize.width, [view1 size].width);
+ EXPECT_FLOAT_EQ(cardSize.height, [view1 size].height);
+ EXPECT_FLOAT_EQ(0.0, [view2 size].width);
+ EXPECT_FLOAT_EQ(0.0, [view2 size].height);
+ EXPECT_FLOAT_EQ(cardSize.width, [view3 size].width);
+ EXPECT_FLOAT_EQ(cardSize.height, [view3 size].height);
+
+ // But it should be automatically updated when it's added again.
+ [stack addCard:view2];
+ EXPECT_FLOAT_EQ(cardSize.width, [view2 size].width);
+ EXPECT_FLOAT_EQ(cardSize.height, [view2 size].height);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, StackSizes) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ [[CardStackLayoutManager alloc] init]);
+ stack.get().layoutIsVertical = boolValues[i];
+ CGRect cardFrame = CGRectMake(0, 0, 100, 200);
+ [stack setCardSize:cardFrame.size];
+ [stack setMaxStagger:30];
+
+ // Asking the size for a collapsed stack should give the same result.
+ CGFloat emptyCollapsedSize = [stack fullyCollapsedStackLength];
+ for (int i = 0; i < 10; ++i) {
+ base::scoped_nsobject<StackCard> card(
+ (StackCard*)[[UIView alloc] initWithFrame:cardFrame]);
+ [stack addCard:card];
+ }
+ CGFloat largeCollapsedSize = [stack fullyCollapsedStackLength];
+ EXPECT_FLOAT_EQ(emptyCollapsedSize, largeCollapsedSize);
+
+ // But a fanned-out stack should get bigger, and the maximum stack size
+ // should be larger still.
+ CGFloat largeExpandedSize = [stack fannedStackLength];
+ EXPECT_GT(largeExpandedSize, largeCollapsedSize);
+ CGFloat largeMaximumSize = [stack maximumStackLength];
+ EXPECT_GT(largeMaximumSize, largeExpandedSize);
+ base::scoped_nsobject<StackCard> card(
+ (StackCard*)[[MockStackCard alloc] init]);
+ [stack addCard:card];
+ CGFloat evenLargerExpandedSize = [stack fannedStackLength];
+ EXPECT_LT(largeExpandedSize, evenLargerExpandedSize);
+ CGFloat evenLargerMaximumSize = [stack maximumStackLength];
+ EXPECT_LT(largeMaximumSize, evenLargerMaximumSize);
+
+ // And start limit shouldn't matter.
+ [stack setStartLimit:10];
+ EXPECT_FLOAT_EQ(emptyCollapsedSize, [stack fullyCollapsedStackLength]);
+ EXPECT_FLOAT_EQ(evenLargerExpandedSize, [stack fannedStackLength]);
+ EXPECT_FLOAT_EQ(evenLargerMaximumSize, [stack maximumStackLength]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, StackLayout) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 30;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_LT(0, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, PreservingPositionsOnCardSizeChange) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_LT(0, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+
+ // Cards should retain their positions after changing the card size.
+ CGSize cardSize = CGSizeMake(kCardWidth + 10, kCardHeight + 10);
+ [stack setCardSize:cardSize];
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_LT(0, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, SwappingPositionsOnOrientationChange) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_LT(0, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+
+ // After changing orientation, cards' layout offsets should be preserved on
+ // the new layout axis.
+ [stack setLayoutIsVertical:![stack layoutIsVertical]];
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_LT(0, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+ }
+}
+TEST_F(CardStackLayoutManagerTest, EndStackRecomputationOnEndLimitChange) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ CGFloat endLimit = [stack maximumStackLength];
+ [stack setEndLimit:endLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, endLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+
+ // Setting smaller end limit should push third card into end stack.
+ endLimit = 2 * kMaxStagger;
+ [stack setEndLimit:endLimit];
+ ValidateCardPositioningConstraints(stack, endLimit, true);
+ EXPECT_EQ(2, [stack firstEndStackCardIndex]);
+
+ // Making it smaller still should push second card into end stack.
+ endLimit = kMaxStagger;
+ [stack setEndLimit:endLimit];
+ ValidateCardPositioningConstraints(stack, endLimit, true);
+ EXPECT_EQ(1, [stack firstEndStackCardIndex]);
+
+ // Making it larger again should re-fanout the end stack cards.
+ endLimit = [stack maximumStackLength];
+ [stack setEndLimit:endLimit];
+ ValidateCardPositioningConstraints(stack, endLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
+ for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, StackLayoutAtSpecificIndex) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 30;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ NSInteger startIndex = 10;
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:startIndex];
+
+ EXPECT_EQ(kCardCount, [[stack cards] count]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(startIndex, [stack lastStartStackCardIndex]);
+ // Take into account start stack when verifying position of card at
+ // |startIndex|.
+ StackCard* startCard = [[stack cards] objectAtIndex:startIndex];
+ EXPECT_FLOAT_EQ(kMargin + [stack staggerOffsetForIndexFromEdge:startIndex],
+ LayoutOffset(stack, startCard.layout.position));
+ NSInteger firstEndStackCardIndex = [stack firstEndStackCardIndex];
+ for (NSInteger i = startIndex + 1; i < firstEndStackCardIndex; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FLOAT_EQ(kMargin + (i - startIndex) * kMaxStagger,
+ LayoutOffset(stack, card.layout.position));
+ }
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, CardIsCovered) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
+ // Since no cards are hidden in the start or end stack, all cards should
+ // be visible (i.e., not covered).
+ for (NSUInteger i = 0; i < kCardCount; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FALSE([stack cardIsCovered:card]);
+ }
+ // Moving the second card to the same location as the third card should
+ // result in the third card covering the second card.
+ StackCard* secondCard = [[stack cards] objectAtIndex:1];
+ StackCard* thirdCard = [[stack cards] objectAtIndex:2];
+ secondCard.layout = thirdCard.layout;
+ EXPECT_TRUE([stack cardIsCovered:secondCard]);
+ EXPECT_FALSE([stack cardIsCovered:thirdCard]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, CardIsCollapsed) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
+ // Since the cards are fully fanned out, no cards should be collapsed.
+ for (NSUInteger i = 0; i < kCardCount; i++) {
+ StackCard* card = [[stack cards] objectAtIndex:i];
+ EXPECT_FALSE([stack cardIsCollapsed:card]);
+ }
+ // Moving the second card to be |minStackStaggerAmount| away from the
+ // third card should result in the second card being collapsed.
+ StackCard* secondCard = [[stack cards] objectAtIndex:1];
+ StackCard* thirdCard = [[stack cards] objectAtIndex:2];
+ LayoutRect collapsedLayout = thirdCard.layout;
+ if ([stack layoutIsVertical])
+ collapsedLayout.position.originY -= [stack minStackStaggerAmount];
+ else
+ collapsedLayout.position.leading -= [stack minStackStaggerAmount];
+ secondCard.layout = collapsedLayout;
+ EXPECT_TRUE([stack cardIsCollapsed:secondCard]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, BasicScroll) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ // Scrolling towards start stack should keep first card anchored, and move
+ // the other two.
+ [stack scrollCardAtIndex:kCardCount - 1
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ // Scrolling back towards end stack should reverse the reverse scroll.
+ [stack scrollCardAtIndex:kCardCount - 1
+ byDelta:10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollCardAwayFromNeighbor) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ // Configure the stack so that the first card is > the scroll-away distance
+ // from the end stack, but the second card is not.
+ const float kEndLimit =
+ [stack scrollCardAwayFromNeighborAmount] + 2 * [stack maxStagger];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scrolling the third card away from the second card should result in it
+ // being placed in the end stack.
+ [stack scrollCardAtIndex:2 awayFromNeighbor:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_EQ(2, [stack firstEndStackCardIndex]);
+
+ // Scrolling the second card away from the first card should result in it
+ // being the min of |maxStagger + scrollAwayAmount, maximumCardSeparation|
+ // away from the first card.
+ [stack scrollCardAtIndex:1 awayFromNeighbor:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ CGFloat separation =
+ std::min([stack maxStagger] + [stack scrollCardAwayFromNeighborAmount],
+ [stack maximumCardSeparation]);
+ EXPECT_FLOAT_EQ(separation, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_EQ(2, [stack firstEndStackCardIndex]);
+
+ // Scrolling the second card away from the third card should result in it
+ // being the min of |maxStagger + scrollAwayAmount, maximumCardSeparation|
+ // away from the third card.
+ separation = std::min([stack maximumCardSeparation],
+ SeparationOnLayoutAxis(stack, 1, 2) +
+ [stack scrollCardAwayFromNeighborAmount]);
+ [stack scrollCardAtIndex:1 awayFromNeighbor:NO];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(separation, SeparationOnLayoutAxis(stack, 1, 2));
+ EXPECT_EQ(2, [stack firstEndStackCardIndex]);
+
+ // Scrolling the third card away from the end stack should result in it
+ // being |scrollAwayAmount| away from the endLimit and not being in the
+ // end stack.
+ StackCard* thirdCard = [[stack cards] objectAtIndex:2];
+ separation =
+ std::min([stack maximumCardSeparation],
+ kEndLimit - LayoutOffset(stack, thirdCard.layout.position) +
+ [stack scrollCardAwayFromNeighborAmount]);
+ [stack scrollCardAtIndex:2 awayFromNeighbor:NO];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(separation,
+ kEndLimit - LayoutOffset(stack, thirdCard.layout.position));
+ EXPECT_EQ(3, [stack firstEndStackCardIndex]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollNotScrollingLeadingCards) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 4;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ // Make the stack large enough to fan out all its cards to avoid having to
+ // worry about the end stack below.
+ const float kEndLimit = [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 2, 3));
+
+ // Scrolling third card toward start stack without scrolling leading cards
+ // should result in third and fourth cards scrolling, but second card not
+ // scrolling.
+ [stack scrollCardAtIndex:2
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:NO];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 1, 2));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 2, 3));
+
+ // Doing the same toward the end stack should have the opposite effect.
+ [stack fanOutCardsWithStartIndex:0];
+ // First give the cards some room to scroll away from the start stack.
+ [stack scrollCardAtIndex:3
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ [stack scrollCardAtIndex:2
+ byDelta:10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:NO];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 2, 3));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollCollapseExpansionOfLargeStack) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 10;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack stackIsFullyFannedOut]);
+ EXPECT_FALSE([stack stackIsFullyCollapsed]);
+ EXPECT_FALSE([stack stackIsFullyOverextended]);
+
+ // Test fanning out/overextension toward end stack.
+ [stack scrollCardAtIndex:0
+ byDelta:-10
+ allowEarlyOverscroll:NO
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack stackIsFullyFannedOut]);
+ EXPECT_FALSE([stack stackIsFullyCollapsed]);
+ EXPECT_FALSE([stack stackIsFullyOverextended]);
+
+ [stack scrollCardAtIndex:0
+ byDelta:[stack maximumStackLength]
+ allowEarlyOverscroll:NO
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack stackIsFullyFannedOut]);
+ EXPECT_FALSE([stack stackIsFullyCollapsed]);
+ EXPECT_TRUE([stack stackIsFullyOverextended]);
+
+ // Test collapsing/overextension toward start stack.
+ [stack fanOutCardsWithStartIndex:0];
+ [stack scrollCardAtIndex:0
+ byDelta:-2.0 * [stack maximumStackLength]
+ allowEarlyOverscroll:NO
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack stackIsFullyFannedOut]);
+ EXPECT_TRUE([stack stackIsFullyCollapsed]);
+ EXPECT_TRUE([stack stackIsFullyOverextended]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollCollapseExpansionOfStackCornerCases) {
+ BOOL boolValues[2] = {NO, YES};
+ const unsigned int kCardCount = 1;
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ // A laid-out stack with one card is fully collapsed and fully fanned out,
+ // but not fully overextended.
+ [stack fanOutCardsWithStartIndex:0];
+ EXPECT_TRUE([stack stackIsFullyFannedOut]);
+ EXPECT_TRUE([stack stackIsFullyCollapsed]);
+ EXPECT_FALSE([stack stackIsFullyOverextended]);
+
+ // A stack with no cards is fully collapsed, fully fanned out, and fully
+ // overextended.
+ [stack removeCard:[[stack cards] objectAtIndex:0]];
+ EXPECT_TRUE([stack stackIsFullyFannedOut]);
+ EXPECT_TRUE([stack stackIsFullyCollapsed]);
+ EXPECT_TRUE([stack stackIsFullyOverextended]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, OneCardOverscroll) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 1;
+ const float kScrollAwayAmount = 20.0;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling toward end should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+
+ // Eliminate the overscroll to test scrolling toward start.
+ [stack eliminateOverextension];
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling toward start should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:-kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin - kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, MaximumOverextensionAmount) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 1;
+ const float kScrollAwayAmount = 20.0;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling toward start/end should have no impact.
+ [stack setMaximumOverextensionAmount:0];
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(0, [stack overextensionAmount]);
+
+ [stack scrollCardAtIndex:0
+ byDelta:-kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(0, [stack overextensionAmount]);
+
+ // Setting a maximum overextension amount > 0 should allow overscrolling to
+ // that limit.
+ CGFloat maxOverextensionAmount = kScrollAwayAmount / 2.0;
+ [stack setMaximumOverextensionAmount:maxOverextensionAmount];
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:NO
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin + maxOverextensionAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(maxOverextensionAmount, [stack overextensionAmount]);
+
+ // Eliminate the overscroll to test scrolling toward start.
+ [stack eliminateOverextension];
+
+ [stack scrollCardAtIndex:0
+ byDelta:-kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:NO
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin - maxOverextensionAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(maxOverextensionAmount, [stack overextensionAmount]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, DecayOnOverscroll) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 1;
+ const float kScrollAwayAmount = 10.0;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling toward end by |kScrollAwayAmount| should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling again by |kScrollAwayAmount| with no decay on overscroll
+ // should result in another move of |kScrollAwayAmount|.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:NO
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin + 2 * kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+
+ // Scrolling by |kScrollAwayAmount| a third time *with* decay on overscroll
+ // should result in a move of less than |kScrollAwayAmount|.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_GT(kMargin + 3 * kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, EliminateOverextension) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 2;
+ const float kScrollAwayAmount = 20.0;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+ StackCard* secondCard = [[stack cards] objectAtIndex:1];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+
+ CGFloat firstCardInitialOrigin =
+ LayoutOffset(stack, firstCard.layout.position);
+ CGFloat secondCardInitialOrigin =
+ LayoutOffset(stack, secondCard.layout.position);
+
+ // Scrolling toward end should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin + kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
+ LayoutOffset(stack, secondCard.layout.position));
+
+ // Calling |eliminateOverextension| should undo the overscroll on the first
+ // and second card.
+ [stack eliminateOverextension];
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin,
+ LayoutOffset(stack, secondCard.layout.position));
+
+ // Scrolling toward start should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:-kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin - kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin - kScrollAwayAmount,
+ LayoutOffset(stack, secondCard.layout.position));
+
+ // Calling |eliminateOverextension| should undo the overscroll on the first
+ // card but leave the second card as-is, since it's not overscrolled.
+ [stack eliminateOverextension];
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin - kScrollAwayAmount,
+ LayoutOffset(stack, secondCard.layout.position));
+
+ // Reset state to test pinch.
+ [stack fanOutCardsWithStartIndex:0];
+
+ // Pinching first card toward end should result in it being overextended.
+ [stack handleMultitouchWithFirstDelta:kScrollAwayAmount
+ secondDelta:kScrollAwayAmount
+ firstCardIndex:0
+ secondCardIndex:1
+ decayOnOverpinch:YES];
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin + kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
+ LayoutOffset(stack, secondCard.layout.position));
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+
+ // ELiminating the overextension, which is now an overpinch, should restore
+ // the first card to its initial position but not alter the offset of the
+ // second card.
+ [stack eliminateOverextension];
+ EXPECT_FLOAT_EQ(firstCardInitialOrigin,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
+ LayoutOffset(stack, secondCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, MultiCardOverscroll) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ const float kScrollAwayAmount = 100.0;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ // Scrolling away from the start stack should result in overscroll.
+ [stack scrollCardAtIndex:0
+ byDelta:kScrollAwayAmount
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
+ LayoutOffset(stack, firstCard.layout.position));
+
+ // Calling |eliminateOverextension| should restore the stack to its previous
+ // fanned-out state.
+ [stack eliminateOverextension];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scrolling toward the start more than is necessary to fully collapse the
+ // stack should result in overscroll toward the start.
+ CGFloat lastCardCollapsedPosition =
+ kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
+ StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
+ CGFloat distanceToCollapsedStack =
+ lastCardCollapsedPosition -
+ LayoutOffset(stack, lastCard.layout.position);
+ [stack scrollCardAtIndex:kCardCount - 1
+ byDelta:distanceToCollapsedStack - 10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FLOAT_EQ(kMargin - 10,
+ LayoutOffset(stack, firstCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, Fling) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Flinging on the first card should not result in overscroll...
+ [stack scrollCardAtIndex:0
+ byDelta:-20.0
+ allowEarlyOverscroll:NO
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ // ... until the last card becomes overscrolled.
+ [stack scrollCardAtIndex:0
+ byDelta:-[stack maximumStackLength]
+ allowEarlyOverscroll:NO
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAroundStartStack) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ // Scroll second card into start stack.
+ CGFloat cardTwoStartStackOffset =
+ kMargin + [stack staggerOffsetForIndexFromEdge:1];
+ LayoutRectPosition cardTwoPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:1]).layout.position;
+ [stack scrollCardAtIndex:1
+ byDelta:cardTwoStartStackOffset -
+ LayoutOffset(stack, cardTwoPosition)
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(1, [stack lastStartStackCardIndex]);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scroll third card toward start stack, and check that everything is as
+ // expected.
+ [stack scrollCardAtIndex:2
+ byDelta:kMaxStagger / -2.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(1, [stack lastStartStackCardIndex]);
+ EXPECT_FLOAT_EQ(kMaxStagger / 2.0, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scroll third card away from stack stack, and check that second card
+ // doesn't come out before it should.
+ [stack scrollCardAtIndex:2
+ byDelta:kMaxStagger / 4.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(1, [stack lastStartStackCardIndex]);
+ EXPECT_FLOAT_EQ(kMaxStagger * .75, SeparationOnLayoutAxis(stack, 1, 2));
+ [stack scrollCardAtIndex:2
+ byDelta:kMaxStagger / 4.0 + 1
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ(0, [stack lastStartStackCardIndex]);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAroundEndStack) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 7;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit = 0.2 * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:4];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
+ EXPECT_EQ(4, [stack lastStartStackCardIndex]);
+ EXPECT_EQ(7, [stack firstEndStackCardIndex]);
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ // Scroll seventh card into end stack.
+ CGFloat cardSevenEndStackOffset =
+ kEndLimit - ([stack staggerOffsetForIndexFromEdge:0] +
+ [stack minStackStaggerAmount]);
+ LayoutRectPosition cardSevenPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:6]).layout.position;
+ [stack scrollCardAtIndex:6
+ byDelta:cardSevenEndStackOffset -
+ LayoutOffset(stack, cardSevenPosition)
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ([stack firstEndStackCardIndex], 6);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
+
+ // Scroll sixth card toward end stack, and check that everything is as
+ // expected.
+ [stack scrollCardAtIndex:5
+ byDelta:kMaxStagger / 2.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ([stack firstEndStackCardIndex], 6);
+ EXPECT_FLOAT_EQ(kMaxStagger / 2.0, SeparationOnLayoutAxis(stack, 5, 6));
+
+ // Scroll sixth card away from end stack, and check that seventh card
+ // doesn't come out before it should.
+ [stack scrollCardAtIndex:5
+ byDelta:kMaxStagger / -4.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ([stack firstEndStackCardIndex], 6);
+ EXPECT_FLOAT_EQ(SeparationOnLayoutAxis(stack, 5, 6), kMaxStagger * .75);
+ [stack scrollCardAtIndex:5
+ byDelta:kMaxStagger / -4.0 - 1
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_EQ([stack firstEndStackCardIndex], 7);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, BasicMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ [stack handleMultitouchWithFirstDelta:-10
+ secondDelta:50
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger + 60, SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, MultitouchBoundedByNeighbor) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ // Make sure that the stack end limit isn't hit in this test.
+ const float kEndLimit = 2.0 * [stack maximumStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ CGFloat cardSize = (i > 0) ? kCardHeight : kCardWidth;
+
+ // Verify that it's not possible to pinch the third card entirely off the
+ // second card.
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:2.0 * cardSize
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Verify that it's not possible to pinch the second card entirely off the
+ // third card.
+ [stack handleMultitouchWithFirstDelta:-2.0 * cardSize
+ secondDelta:0
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Verify that it's not possible to pinch the two cards off each other.
+ [stack handleMultitouchWithFirstDelta:-cardSize
+ secondDelta:cardSize
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, OverpinchTowardStart) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 2;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+ LayoutRectPosition firstCardPosition = firstCard.layout.position;
+ StackCard* secondCard = [[stack cards] objectAtIndex:1];
+ LayoutRectPosition secondCardPosition = secondCard.layout.position;
+
+ [stack handleMultitouchWithFirstDelta:-20
+ secondDelta:10
+ firstCardIndex:0
+ secondCardIndex:1
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // First card should have been overpinched.
+ EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
+ EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
+ EXPECT_FLOAT_EQ(LayoutOffset(stack, firstCardPosition) - 20,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(LayoutOffset(stack, secondCardPosition) + 10,
+ LayoutOffset(stack, secondCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, OverpinchTowardEnd) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 2;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ StackCard* firstCard = [[stack cards] objectAtIndex:0];
+ LayoutRectPosition firstCardPosition = firstCard.layout.position;
+ StackCard* secondCard = [[stack cards] objectAtIndex:1];
+ LayoutRectPosition secondCardPosition = secondCard.layout.position;
+
+ [stack handleMultitouchWithFirstDelta:20
+ secondDelta:10
+ firstCardIndex:0
+ secondCardIndex:1
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Both first and second card should have moved.
+ EXPECT_FLOAT_EQ(LayoutOffset(stack, firstCardPosition) + 20,
+ LayoutOffset(stack, firstCard.layout.position));
+ EXPECT_FLOAT_EQ(LayoutOffset(stack, secondCardPosition) + 10,
+ LayoutOffset(stack, secondCard.layout.position));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, StressMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 30;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ [stack handleMultitouchWithFirstDelta:-10
+ secondDelta:50
+ firstCardIndex:5
+ secondCardIndex:10
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+
+ [stack handleMultitouchWithFirstDelta:20
+ secondDelta:-10
+ firstCardIndex:3
+ secondCardIndex:15
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+
+ [stack handleMultitouchWithFirstDelta:-20
+ secondDelta:-10
+ firstCardIndex:0
+ secondCardIndex:4
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAfterMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ const float kPinchDistance = 50;
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kPinchDistance
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ [stack scrollCardAtIndex:2
+ byDelta:10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+ [stack scrollCardAtIndex:2
+ byDelta:-20
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollEveningOutAfterMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ const float kPinchDistance = kMaxStagger / 2.0;
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scroll cards toward start stack to give some room to scroll second card
+ // toward end stack (see below).
+ [stack scrollCardAtIndex:1
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Pinch the third card closer to the second card.
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:-kPinchDistance
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger - kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Separation between cards should be maintained when second card is
+ // scrolled towards third card.
+ [stack scrollCardAtIndex:1
+ byDelta:10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger - kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scrolling second card away from third card by the distance that the
+ // third card was pinched should restore separation of |kMaxStagger|
+ // between the second and third card.
+ [stack scrollCardAtIndex:1
+ byDelta:-kPinchDistance
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAroundStartStackAfterMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ const float kPinchDistance = 50;
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kPinchDistance
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ [stack scrollCardAtIndex:2
+ byDelta:-20
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ // Scroll the cards completely into the start stack.
+ CGFloat lastCardCollapsedPosition =
+ kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
+ StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
+ CGFloat distanceToCollapsedStack =
+ lastCardCollapsedPosition -
+ LayoutOffset(stack, lastCard.layout.position);
+ [stack scrollCardAtIndex:2
+ byDelta:distanceToCollapsedStack
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ EXPECT_EQ((NSInteger)(kCardCount - 1), [stack lastStartStackCardIndex]);
+ // Scroll the cards out of the start stack: they should now be separated by
+ // |kMaxStagger|.
+ [stack scrollCardAtIndex:2
+ byDelta:2 * kMaxStagger
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAroundEndStackAfterMultitouch) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 7;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit = 0.3 * [stack fannedStackLength];
+ const float kPinchDistance = 20;
+ [stack setEndLimit:kEndLimit];
+ // Start in the middle of the stack to be able to scroll cards into the end
+ // stack.
+ [stack fanOutCardsWithStartIndex:4];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kPinchDistance
+ firstCardIndex:5
+ secondCardIndex:6
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 5, 6));
+
+ [stack scrollCardAtIndex:5
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 5, 6));
+ // Scroll the two cards in question into the end stack.
+ CGFloat cardSixEndStackPosition =
+ kEndLimit - [stack staggerOffsetForIndexFromEdge:1];
+ LayoutRectPosition cardSixPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
+ [stack scrollCardAtIndex:5
+ byDelta:cardSixEndStackPosition -
+ LayoutOffset(stack, cardSixPosition)
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ cardSixPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_EQ(5, [stack firstEndStackCardIndex]);
+ // Scroll the cards out of the end stack: cards should now be
+ // separated by |kMaxStagger|.
+ [stack scrollCardAtIndex:5
+ byDelta:-2.0 * kMaxStagger
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ EXPECT_FLOAT_EQ(SeparationOnLayoutAxis(stack, 5, 6), kMaxStagger);
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAfterPinchOutOfStartStack) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 3;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit =
+ kDefaultEndLimitFraction * [stack fannedStackLength];
+ const float kPinchDistance = 50;
+ [stack setEndLimit:kEndLimit];
+ [stack fanOutCardsWithStartIndex:0];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kPinchDistance
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+
+ [stack scrollCardAtIndex:1
+ byDelta:-20
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 1, 2));
+ // Scroll the cards completely into the start stack.
+ CGFloat lastCardCollapsedPosition =
+ kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
+ StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
+ CGFloat distanceToCollapsedStack =
+ lastCardCollapsedPosition -
+ LayoutOffset(stack, lastCard.layout.position);
+ [stack scrollCardAtIndex:kCardCount - 1
+ byDelta:distanceToCollapsedStack
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ EXPECT_EQ((NSInteger)(kCardCount - 1), [stack lastStartStackCardIndex]);
+ CGFloat inStackSeparation = SeparationOnLayoutAxis(stack, 1, 2);
+ // Pinch the third card far out of the start stack.
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kMaxStagger
+ firstCardIndex:1
+ secondCardIndex:2
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(inStackSeparation + kMaxStagger,
+ SeparationOnLayoutAxis(stack, 1, 2));
+ // A scroll should immediately bring the second card out of the start
+ // stack, without affecting the distance between the second and third cards.
+ [stack scrollCardAtIndex:2
+ byDelta:kMaxStagger / 2.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(inStackSeparation + kMaxStagger,
+ SeparationOnLayoutAxis(stack, 1, 2));
+ }
+}
+
+TEST_F(CardStackLayoutManagerTest, ScrollAfterPinchOutOfEndStack) {
+ BOOL boolValues[2] = {NO, YES};
+ for (unsigned long i = 0; i < arraysize(boolValues); i++) {
+ const unsigned int kCardCount = 7;
+ base::scoped_nsobject<CardStackLayoutManager> stack(
+ newStackOfNCards(kCardCount, boolValues[i]));
+
+ const float kEndLimit = 0.3 * [stack fannedStackLength];
+ const float kPinchDistance = 20;
+ [stack setEndLimit:kEndLimit];
+ // Start in the middle of the stack to be able to scroll cards into the end
+ // stack.
+ [stack fanOutCardsWithStartIndex:4];
+
+ EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
+ ValidateCardPositioningConstraints(stack, kEndLimit, true);
+
+ [stack handleMultitouchWithFirstDelta:0
+ secondDelta:kPinchDistance
+ firstCardIndex:5
+ secondCardIndex:6
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 5, 6));
+
+ [stack scrollCardAtIndex:5
+ byDelta:-10
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ // Separation between cards should be maintained.
+ EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
+ SeparationOnLayoutAxis(stack, 5, 6));
+
+ // Scroll the two cards in question into the end stack.
+ CGFloat cardSixEndStackOffset =
+ kEndLimit - ([stack staggerOffsetForIndexFromEdge:1] +
+ [stack minStackStaggerAmount]);
+ LayoutRectPosition cardSixPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
+ [stack scrollCardAtIndex:5
+ byDelta:cardSixEndStackOffset -
+ LayoutOffset(stack, cardSixPosition)
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ cardSixPosition =
+ ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_EQ(5, [stack firstEndStackCardIndex]);
+ CGFloat inStackSeparation = SeparationOnLayoutAxis(stack, 5, 6);
+ // Pinch the sixth card far out of the start stack.
+ [stack handleMultitouchWithFirstDelta:-2.0 * kMaxStagger
+ secondDelta:0
+ firstCardIndex:5
+ secondCardIndex:6
+ decayOnOverpinch:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_EQ(6, [stack firstEndStackCardIndex]);
+ EXPECT_FLOAT_EQ(inStackSeparation + 2 * kMaxStagger,
+ SeparationOnLayoutAxis(stack, 5, 6));
+ // A scroll should immediately bring the seventh card out of the start
+ // stack, without affecting the distance between the sixth and seventh
+ // cards.
+ [stack scrollCardAtIndex:5
+ byDelta:kMaxStagger / -2.0
+ allowEarlyOverscroll:YES
+ decayOnOverscroll:YES
+ scrollLeadingCards:YES];
+ ValidateCardPositioningConstraints(stack, kEndLimit, false);
+ EXPECT_EQ(7, [stack firstEndStackCardIndex]);
+ EXPECT_FLOAT_EQ(inStackSeparation + 2 * kMaxStagger,
+ SeparationOnLayoutAxis(stack, 5, 6));
+ }
+}
+
+} // namespace

Powered by Google App Engine
This is Rietveld 408576698