| 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
|
|
|