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

Side by Side 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 unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "base/mac/scoped_nsobject.h"
6 #include "ios/chrome/browser/ui/rtl_geometry.h"
7 #import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h"
8 #import "ios/chrome/browser/ui/stack_view/stack_card.h"
9 #include "testing/gtest/include/gtest/gtest.h"
10 #include "testing/platform_test.h"
11
12 @interface CardStackLayoutManager (Private)
13 - (CGFloat)minStackStaggerAmount;
14 - (CGFloat)scrollCardAwayFromNeighborAmount;
15 - (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge;
16 - (CGFloat)maximumCardSeparation;
17 @end
18
19 // A mock version of StackCard.
20 @interface MockStackCard : NSObject
21
22 @property(nonatomic, readwrite, assign) BOOL synchronizeView;
23 @property(nonatomic, readwrite, assign) LayoutRect layout;
24 @property(nonatomic, readwrite, assign) CGSize size;
25
26 @end
27
28 @implementation MockStackCard
29
30 @synthesize synchronizeView = _synchronizeView;
31 @synthesize layout = _layout;
32 @synthesize size = _size;
33
34 - (void)setSize:(CGSize)size {
35 _layout.position.leading += (_layout.size.width - size.width) / 2.0;
36 _layout.position.originY += (_layout.size.height - size.height) / 2.0;
37 _layout.size = size;
38 _size = size;
39 }
40
41 @end
42
43 namespace {
44
45 // Create a fixture to get an autorelease pool.
46 class CardStackLayoutManagerTest : public PlatformTest {};
47
48 const float kMargin = 5;
49 const float kMaxStagger = 40;
50 const float kAxisPosition = 55;
51 const float kCardWidth = 300;
52 const float kCardHeight = 400;
53 const float kDefaultEndLimitFraction = 0.4;
54
55 // Returns the offset of |point| in the current layout direction.
56 CGFloat LayoutOffset(CardStackLayoutManager* stack,
57 LayoutRectPosition position) {
58 return [stack layoutIsVertical] ? position.originY : position.leading;
59 }
60
61 // Returns the distance along the layout axis between the cards at |firstIndex|
62 // and |secondIndex|.
63 CGFloat SeparationOnLayoutAxis(CardStackLayoutManager* stack,
64 NSUInteger firstIndex,
65 NSUInteger secondIndex) {
66 StackCard* firstCard = [[stack cards] objectAtIndex:firstIndex];
67 StackCard* secondCard = [[stack cards] objectAtIndex:secondIndex];
68 CGFloat firstCardOffset = LayoutOffset(stack, firstCard.layout.position);
69 CGFloat secondCardOffset = LayoutOffset(stack, secondCard.layout.position);
70 return secondCardOffset - firstCardOffset;
71 }
72
73 // Validates basic constraints:
74 // - All cards should be centered at kAxisPosition along the non-layout axis.
75 // - If not overscrolled toward start, all start edges should be at or past
76 // kMargin.
77 // - All start edges should be visibly before endLimit.
78 // - No card should start before a previous card.
79 // - No card should start after the end of a previous card.
80 // If |shouldBeWithinMaxStagger|:
81 // - Consecutive cards should be no more than kMaxStagger apart.
82 void ValidateCardPositioningConstraints(CardStackLayoutManager* stack,
83 CGFloat endLimit,
84 bool shouldBeWithinMaxStagger) {
85 BOOL isVertical = [stack layoutIsVertical];
86 StackCard* previousCard = nil;
87 for (StackCard* card in [stack cards]) {
88 CGRect cardFrame = LayoutRectGetRect(card.layout);
89 CGFloat nonLayoutAxisCenter =
90 isVertical ? CGRectGetMidX(cardFrame) : CGRectGetMidY(cardFrame);
91 EXPECT_FLOAT_EQ(kAxisPosition, nonLayoutAxisCenter);
92 CGFloat startEdge = LayoutOffset(stack, card.layout.position);
93 if (![stack overextensionTowardStartOnCardAtIndex:0])
94 EXPECT_LE(kMargin, startEdge);
95 EXPECT_GT(endLimit, startEdge);
96 if (previousCard != nil) {
97 CGFloat previousTopEdge =
98 LayoutOffset(stack, previousCard.layout.position);
99 EXPECT_LE(previousTopEdge, startEdge);
100 CGFloat cardSize = isVertical ? kCardHeight : kCardWidth;
101 EXPECT_GE(cardSize, startEdge - previousTopEdge);
102 if (shouldBeWithinMaxStagger)
103 EXPECT_GE(kMaxStagger, startEdge - previousTopEdge);
104 }
105 previousCard = card;
106 }
107 }
108
109 // Creates a new |CardStackLayoutManager|, adds |n| cards to it, and sets
110 // dimensional/positioning parameters to the constants defined above.
111 CardStackLayoutManager* newStackOfNCards(unsigned int n, BOOL layoutIsVertical)
112 NS_RETURNS_RETAINED {
113 CardStackLayoutManager* stack = [[CardStackLayoutManager alloc] init];
114 stack.layoutIsVertical = layoutIsVertical;
115 for (unsigned int i = 0; i < n; ++i) {
116 base::scoped_nsobject<StackCard> card(
117 (StackCard*)[[MockStackCard alloc] init]);
118 [stack addCard:card];
119 }
120
121 CGSize cardSize = CGSizeMake(kCardWidth, kCardHeight);
122 [stack setCardSize:cardSize];
123 [stack setStartLimit:kMargin];
124 [stack setMaxStagger:kMaxStagger];
125 [stack setLayoutAxisPosition:kAxisPosition];
126 CGFloat cardLength = layoutIsVertical ? cardSize.height : cardSize.width;
127 [stack setMaximumOverextensionAmount:cardLength / 2.0];
128
129 return stack;
130 }
131
132 TEST_F(CardStackLayoutManagerTest, CardSizing) {
133 BOOL boolValues[2] = {NO, YES};
134 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
135 base::scoped_nsobject<CardStackLayoutManager> stack(
136 [[CardStackLayoutManager alloc] init]);
137 stack.get().layoutIsVertical = boolValues[i];
138
139 base::scoped_nsobject<StackCard> view1(
140 (StackCard*)[[MockStackCard alloc] init]);
141 base::scoped_nsobject<StackCard> view2(
142 (StackCard*)[[MockStackCard alloc] init]);
143 base::scoped_nsobject<StackCard> view3(
144 (StackCard*)[[MockStackCard alloc] init]);
145 [stack addCard:view1.get()];
146 [stack addCard:view2.get()];
147 [stack addCard:view3.get()];
148 // Ensure that removed cards are not altered.
149 [stack removeCard:view2];
150
151 CGSize cardSize = CGSizeMake(111, 222);
152 [stack setCardSize:cardSize];
153
154 EXPECT_FLOAT_EQ(cardSize.width, [view1 size].width);
155 EXPECT_FLOAT_EQ(cardSize.height, [view1 size].height);
156 EXPECT_FLOAT_EQ(0.0, [view2 size].width);
157 EXPECT_FLOAT_EQ(0.0, [view2 size].height);
158 EXPECT_FLOAT_EQ(cardSize.width, [view3 size].width);
159 EXPECT_FLOAT_EQ(cardSize.height, [view3 size].height);
160
161 // But it should be automatically updated when it's added again.
162 [stack addCard:view2];
163 EXPECT_FLOAT_EQ(cardSize.width, [view2 size].width);
164 EXPECT_FLOAT_EQ(cardSize.height, [view2 size].height);
165 }
166 }
167
168 TEST_F(CardStackLayoutManagerTest, StackSizes) {
169 BOOL boolValues[2] = {NO, YES};
170 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
171 base::scoped_nsobject<CardStackLayoutManager> stack(
172 [[CardStackLayoutManager alloc] init]);
173 stack.get().layoutIsVertical = boolValues[i];
174 CGRect cardFrame = CGRectMake(0, 0, 100, 200);
175 [stack setCardSize:cardFrame.size];
176 [stack setMaxStagger:30];
177
178 // Asking the size for a collapsed stack should give the same result.
179 CGFloat emptyCollapsedSize = [stack fullyCollapsedStackLength];
180 for (int i = 0; i < 10; ++i) {
181 base::scoped_nsobject<StackCard> card(
182 (StackCard*)[[UIView alloc] initWithFrame:cardFrame]);
183 [stack addCard:card];
184 }
185 CGFloat largeCollapsedSize = [stack fullyCollapsedStackLength];
186 EXPECT_FLOAT_EQ(emptyCollapsedSize, largeCollapsedSize);
187
188 // But a fanned-out stack should get bigger, and the maximum stack size
189 // should be larger still.
190 CGFloat largeExpandedSize = [stack fannedStackLength];
191 EXPECT_GT(largeExpandedSize, largeCollapsedSize);
192 CGFloat largeMaximumSize = [stack maximumStackLength];
193 EXPECT_GT(largeMaximumSize, largeExpandedSize);
194 base::scoped_nsobject<StackCard> card(
195 (StackCard*)[[MockStackCard alloc] init]);
196 [stack addCard:card];
197 CGFloat evenLargerExpandedSize = [stack fannedStackLength];
198 EXPECT_LT(largeExpandedSize, evenLargerExpandedSize);
199 CGFloat evenLargerMaximumSize = [stack maximumStackLength];
200 EXPECT_LT(largeMaximumSize, evenLargerMaximumSize);
201
202 // And start limit shouldn't matter.
203 [stack setStartLimit:10];
204 EXPECT_FLOAT_EQ(emptyCollapsedSize, [stack fullyCollapsedStackLength]);
205 EXPECT_FLOAT_EQ(evenLargerExpandedSize, [stack fannedStackLength]);
206 EXPECT_FLOAT_EQ(evenLargerMaximumSize, [stack maximumStackLength]);
207 }
208 }
209
210 TEST_F(CardStackLayoutManagerTest, StackLayout) {
211 BOOL boolValues[2] = {NO, YES};
212 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
213 const unsigned int kCardCount = 30;
214 base::scoped_nsobject<CardStackLayoutManager> stack(
215 newStackOfNCards(kCardCount, boolValues[i]));
216
217 const float kEndLimit =
218 kDefaultEndLimitFraction * [stack fannedStackLength];
219 [stack setEndLimit:kEndLimit];
220 [stack fanOutCardsWithStartIndex:0];
221
222 EXPECT_EQ(kCardCount, [[stack cards] count]);
223 ValidateCardPositioningConstraints(stack, kEndLimit, true);
224 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
225 EXPECT_LT(0, [stack firstEndStackCardIndex]);
226 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
227 StackCard* card = [[stack cards] objectAtIndex:i];
228 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
229 LayoutOffset(stack, card.layout.position));
230 }
231 }
232 }
233
234 TEST_F(CardStackLayoutManagerTest, PreservingPositionsOnCardSizeChange) {
235 BOOL boolValues[2] = {NO, YES};
236 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
237 const unsigned int kCardCount = 3;
238 base::scoped_nsobject<CardStackLayoutManager> stack(
239 newStackOfNCards(kCardCount, boolValues[i]));
240
241 const float kEndLimit =
242 kDefaultEndLimitFraction * [stack fannedStackLength];
243 [stack setEndLimit:kEndLimit];
244 [stack fanOutCardsWithStartIndex:0];
245
246 EXPECT_EQ(kCardCount, [[stack cards] count]);
247 ValidateCardPositioningConstraints(stack, kEndLimit, true);
248 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
249 EXPECT_LT(0, [stack firstEndStackCardIndex]);
250 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
251 StackCard* card = [[stack cards] objectAtIndex:i];
252 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
253 LayoutOffset(stack, card.layout.position));
254 }
255
256 // Cards should retain their positions after changing the card size.
257 CGSize cardSize = CGSizeMake(kCardWidth + 10, kCardHeight + 10);
258 [stack setCardSize:cardSize];
259 EXPECT_EQ(kCardCount, [[stack cards] count]);
260 ValidateCardPositioningConstraints(stack, kEndLimit, true);
261 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
262 EXPECT_LT(0, [stack firstEndStackCardIndex]);
263 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
264 StackCard* card = [[stack cards] objectAtIndex:i];
265 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
266 LayoutOffset(stack, card.layout.position));
267 }
268 }
269 }
270
271 TEST_F(CardStackLayoutManagerTest, SwappingPositionsOnOrientationChange) {
272 BOOL boolValues[2] = {NO, YES};
273 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
274 const unsigned int kCardCount = 3;
275 base::scoped_nsobject<CardStackLayoutManager> stack(
276 newStackOfNCards(kCardCount, boolValues[i]));
277
278 const float kEndLimit =
279 kDefaultEndLimitFraction * [stack fannedStackLength];
280 [stack setEndLimit:kEndLimit];
281 [stack fanOutCardsWithStartIndex:0];
282
283 EXPECT_EQ(kCardCount, [[stack cards] count]);
284 ValidateCardPositioningConstraints(stack, kEndLimit, true);
285 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
286 EXPECT_LT(0, [stack firstEndStackCardIndex]);
287 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
288 StackCard* card = [[stack cards] objectAtIndex:i];
289 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
290 LayoutOffset(stack, card.layout.position));
291 }
292
293 // After changing orientation, cards' layout offsets should be preserved on
294 // the new layout axis.
295 [stack setLayoutIsVertical:![stack layoutIsVertical]];
296 EXPECT_EQ(kCardCount, [[stack cards] count]);
297 ValidateCardPositioningConstraints(stack, kEndLimit, true);
298 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
299 EXPECT_LT(0, [stack firstEndStackCardIndex]);
300 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
301 StackCard* card = [[stack cards] objectAtIndex:i];
302 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
303 LayoutOffset(stack, card.layout.position));
304 }
305 }
306 }
307 TEST_F(CardStackLayoutManagerTest, EndStackRecomputationOnEndLimitChange) {
308 BOOL boolValues[2] = {NO, YES};
309 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
310 const unsigned int kCardCount = 3;
311 base::scoped_nsobject<CardStackLayoutManager> stack(
312 newStackOfNCards(kCardCount, boolValues[i]));
313
314 CGFloat endLimit = [stack maximumStackLength];
315 [stack setEndLimit:endLimit];
316 [stack fanOutCardsWithStartIndex:0];
317
318 EXPECT_EQ(kCardCount, [[stack cards] count]);
319 ValidateCardPositioningConstraints(stack, endLimit, true);
320 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
321 EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
322 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
323 StackCard* card = [[stack cards] objectAtIndex:i];
324 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
325 LayoutOffset(stack, card.layout.position));
326 }
327
328 // Setting smaller end limit should push third card into end stack.
329 endLimit = 2 * kMaxStagger;
330 [stack setEndLimit:endLimit];
331 ValidateCardPositioningConstraints(stack, endLimit, true);
332 EXPECT_EQ(2, [stack firstEndStackCardIndex]);
333
334 // Making it smaller still should push second card into end stack.
335 endLimit = kMaxStagger;
336 [stack setEndLimit:endLimit];
337 ValidateCardPositioningConstraints(stack, endLimit, true);
338 EXPECT_EQ(1, [stack firstEndStackCardIndex]);
339
340 // Making it larger again should re-fanout the end stack cards.
341 endLimit = [stack maximumStackLength];
342 [stack setEndLimit:endLimit];
343 ValidateCardPositioningConstraints(stack, endLimit, true);
344 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
345 EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
346 for (NSInteger i = 0; i < [stack firstEndStackCardIndex]; i++) {
347 StackCard* card = [[stack cards] objectAtIndex:i];
348 EXPECT_FLOAT_EQ(kMargin + i * kMaxStagger,
349 LayoutOffset(stack, card.layout.position));
350 }
351 }
352 }
353
354 TEST_F(CardStackLayoutManagerTest, StackLayoutAtSpecificIndex) {
355 BOOL boolValues[2] = {NO, YES};
356 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
357 const unsigned int kCardCount = 30;
358 base::scoped_nsobject<CardStackLayoutManager> stack(
359 newStackOfNCards(kCardCount, boolValues[i]));
360
361 NSInteger startIndex = 10;
362
363 const float kEndLimit =
364 kDefaultEndLimitFraction * [stack fannedStackLength];
365 [stack setEndLimit:kEndLimit];
366 [stack fanOutCardsWithStartIndex:startIndex];
367
368 EXPECT_EQ(kCardCount, [[stack cards] count]);
369 ValidateCardPositioningConstraints(stack, kEndLimit, true);
370 EXPECT_EQ(startIndex, [stack lastStartStackCardIndex]);
371 // Take into account start stack when verifying position of card at
372 // |startIndex|.
373 StackCard* startCard = [[stack cards] objectAtIndex:startIndex];
374 EXPECT_FLOAT_EQ(kMargin + [stack staggerOffsetForIndexFromEdge:startIndex],
375 LayoutOffset(stack, startCard.layout.position));
376 NSInteger firstEndStackCardIndex = [stack firstEndStackCardIndex];
377 for (NSInteger i = startIndex + 1; i < firstEndStackCardIndex; i++) {
378 StackCard* card = [[stack cards] objectAtIndex:i];
379 EXPECT_FLOAT_EQ(kMargin + (i - startIndex) * kMaxStagger,
380 LayoutOffset(stack, card.layout.position));
381 }
382 }
383 }
384
385 TEST_F(CardStackLayoutManagerTest, CardIsCovered) {
386 BOOL boolValues[2] = {NO, YES};
387 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
388 const unsigned int kCardCount = 3;
389 base::scoped_nsobject<CardStackLayoutManager> stack(
390 newStackOfNCards(kCardCount, boolValues[i]));
391
392 const float kEndLimit =
393 kDefaultEndLimitFraction * [stack fannedStackLength];
394 [stack setEndLimit:kEndLimit];
395 [stack fanOutCardsWithStartIndex:0];
396
397 ValidateCardPositioningConstraints(stack, kEndLimit, true);
398 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
399 EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
400 // Since no cards are hidden in the start or end stack, all cards should
401 // be visible (i.e., not covered).
402 for (NSUInteger i = 0; i < kCardCount; i++) {
403 StackCard* card = [[stack cards] objectAtIndex:i];
404 EXPECT_FALSE([stack cardIsCovered:card]);
405 }
406 // Moving the second card to the same location as the third card should
407 // result in the third card covering the second card.
408 StackCard* secondCard = [[stack cards] objectAtIndex:1];
409 StackCard* thirdCard = [[stack cards] objectAtIndex:2];
410 secondCard.layout = thirdCard.layout;
411 EXPECT_TRUE([stack cardIsCovered:secondCard]);
412 EXPECT_FALSE([stack cardIsCovered:thirdCard]);
413 }
414 }
415
416 TEST_F(CardStackLayoutManagerTest, CardIsCollapsed) {
417 BOOL boolValues[2] = {NO, YES};
418 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
419 const unsigned int kCardCount = 3;
420 base::scoped_nsobject<CardStackLayoutManager> stack(
421 newStackOfNCards(kCardCount, boolValues[i]));
422
423 const float kEndLimit =
424 kDefaultEndLimitFraction * [stack fannedStackLength];
425 [stack setEndLimit:kEndLimit];
426 [stack fanOutCardsWithStartIndex:0];
427
428 ValidateCardPositioningConstraints(stack, kEndLimit, true);
429 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
430 EXPECT_EQ((int)kCardCount, [stack firstEndStackCardIndex]);
431 // Since the cards are fully fanned out, no cards should be collapsed.
432 for (NSUInteger i = 0; i < kCardCount; i++) {
433 StackCard* card = [[stack cards] objectAtIndex:i];
434 EXPECT_FALSE([stack cardIsCollapsed:card]);
435 }
436 // Moving the second card to be |minStackStaggerAmount| away from the
437 // third card should result in the second card being collapsed.
438 StackCard* secondCard = [[stack cards] objectAtIndex:1];
439 StackCard* thirdCard = [[stack cards] objectAtIndex:2];
440 LayoutRect collapsedLayout = thirdCard.layout;
441 if ([stack layoutIsVertical])
442 collapsedLayout.position.originY -= [stack minStackStaggerAmount];
443 else
444 collapsedLayout.position.leading -= [stack minStackStaggerAmount];
445 secondCard.layout = collapsedLayout;
446 EXPECT_TRUE([stack cardIsCollapsed:secondCard]);
447 }
448 }
449
450 TEST_F(CardStackLayoutManagerTest, BasicScroll) {
451 BOOL boolValues[2] = {NO, YES};
452 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
453 const unsigned int kCardCount = 3;
454 base::scoped_nsobject<CardStackLayoutManager> stack(
455 newStackOfNCards(kCardCount, boolValues[i]));
456 StackCard* firstCard = [[stack cards] objectAtIndex:0];
457
458 const float kEndLimit =
459 kDefaultEndLimitFraction * [stack fannedStackLength];
460 [stack setEndLimit:kEndLimit];
461 [stack fanOutCardsWithStartIndex:0];
462 ValidateCardPositioningConstraints(stack, kEndLimit, true);
463 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
464 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
465 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
466 // Scrolling towards start stack should keep first card anchored, and move
467 // the other two.
468 [stack scrollCardAtIndex:kCardCount - 1
469 byDelta:-10
470 allowEarlyOverscroll:YES
471 decayOnOverscroll:YES
472 scrollLeadingCards:YES];
473 ValidateCardPositioningConstraints(stack, kEndLimit, true);
474 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
475 EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 0, 1));
476 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
477 // Scrolling back towards end stack should reverse the reverse scroll.
478 [stack scrollCardAtIndex:kCardCount - 1
479 byDelta:10
480 allowEarlyOverscroll:YES
481 decayOnOverscroll:YES
482 scrollLeadingCards:YES];
483 ValidateCardPositioningConstraints(stack, kEndLimit, true);
484 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
485 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
486 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
487 }
488 }
489
490 TEST_F(CardStackLayoutManagerTest, ScrollCardAwayFromNeighbor) {
491 BOOL boolValues[2] = {NO, YES};
492 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
493 const unsigned int kCardCount = 3;
494 base::scoped_nsobject<CardStackLayoutManager> stack(
495 newStackOfNCards(kCardCount, boolValues[i]));
496 StackCard* firstCard = [[stack cards] objectAtIndex:0];
497
498 // Configure the stack so that the first card is > the scroll-away distance
499 // from the end stack, but the second card is not.
500 const float kEndLimit =
501 [stack scrollCardAwayFromNeighborAmount] + 2 * [stack maxStagger];
502 [stack setEndLimit:kEndLimit];
503 [stack fanOutCardsWithStartIndex:0];
504 ValidateCardPositioningConstraints(stack, kEndLimit, true);
505 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
506 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
507 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
508
509 // Scrolling the third card away from the second card should result in it
510 // being placed in the end stack.
511 [stack scrollCardAtIndex:2 awayFromNeighbor:YES];
512 ValidateCardPositioningConstraints(stack, kEndLimit, false);
513 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
514 EXPECT_EQ(2, [stack firstEndStackCardIndex]);
515
516 // Scrolling the second card away from the first card should result in it
517 // being the min of |maxStagger + scrollAwayAmount, maximumCardSeparation|
518 // away from the first card.
519 [stack scrollCardAtIndex:1 awayFromNeighbor:YES];
520 ValidateCardPositioningConstraints(stack, kEndLimit, false);
521 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
522 CGFloat separation =
523 std::min([stack maxStagger] + [stack scrollCardAwayFromNeighborAmount],
524 [stack maximumCardSeparation]);
525 EXPECT_FLOAT_EQ(separation, SeparationOnLayoutAxis(stack, 0, 1));
526 EXPECT_EQ(2, [stack firstEndStackCardIndex]);
527
528 // Scrolling the second card away from the third card should result in it
529 // being the min of |maxStagger + scrollAwayAmount, maximumCardSeparation|
530 // away from the third card.
531 separation = std::min([stack maximumCardSeparation],
532 SeparationOnLayoutAxis(stack, 1, 2) +
533 [stack scrollCardAwayFromNeighborAmount]);
534 [stack scrollCardAtIndex:1 awayFromNeighbor:NO];
535 ValidateCardPositioningConstraints(stack, kEndLimit, false);
536 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
537 EXPECT_FLOAT_EQ(separation, SeparationOnLayoutAxis(stack, 1, 2));
538 EXPECT_EQ(2, [stack firstEndStackCardIndex]);
539
540 // Scrolling the third card away from the end stack should result in it
541 // being |scrollAwayAmount| away from the endLimit and not being in the
542 // end stack.
543 StackCard* thirdCard = [[stack cards] objectAtIndex:2];
544 separation =
545 std::min([stack maximumCardSeparation],
546 kEndLimit - LayoutOffset(stack, thirdCard.layout.position) +
547 [stack scrollCardAwayFromNeighborAmount]);
548 [stack scrollCardAtIndex:2 awayFromNeighbor:NO];
549 ValidateCardPositioningConstraints(stack, kEndLimit, false);
550 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
551 EXPECT_FLOAT_EQ(separation,
552 kEndLimit - LayoutOffset(stack, thirdCard.layout.position));
553 EXPECT_EQ(3, [stack firstEndStackCardIndex]);
554 }
555 }
556
557 TEST_F(CardStackLayoutManagerTest, ScrollNotScrollingLeadingCards) {
558 BOOL boolValues[2] = {NO, YES};
559 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
560 const unsigned int kCardCount = 4;
561 base::scoped_nsobject<CardStackLayoutManager> stack(
562 newStackOfNCards(kCardCount, boolValues[i]));
563 StackCard* firstCard = [[stack cards] objectAtIndex:0];
564
565 // Make the stack large enough to fan out all its cards to avoid having to
566 // worry about the end stack below.
567 const float kEndLimit = [stack fannedStackLength];
568 [stack setEndLimit:kEndLimit];
569 [stack fanOutCardsWithStartIndex:0];
570 ValidateCardPositioningConstraints(stack, kEndLimit, true);
571 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
572 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
573 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
574 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 2, 3));
575
576 // Scrolling third card toward start stack without scrolling leading cards
577 // should result in third and fourth cards scrolling, but second card not
578 // scrolling.
579 [stack scrollCardAtIndex:2
580 byDelta:-10
581 allowEarlyOverscroll:YES
582 decayOnOverscroll:YES
583 scrollLeadingCards:NO];
584 ValidateCardPositioningConstraints(stack, kEndLimit, true);
585 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
586 EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 1, 2));
587 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 2, 3));
588
589 // Doing the same toward the end stack should have the opposite effect.
590 [stack fanOutCardsWithStartIndex:0];
591 // First give the cards some room to scroll away from the start stack.
592 [stack scrollCardAtIndex:3
593 byDelta:-10
594 allowEarlyOverscroll:YES
595 decayOnOverscroll:YES
596 scrollLeadingCards:YES];
597 [stack scrollCardAtIndex:2
598 byDelta:10
599 allowEarlyOverscroll:YES
600 decayOnOverscroll:YES
601 scrollLeadingCards:NO];
602 ValidateCardPositioningConstraints(stack, kEndLimit, true);
603 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
604 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
605 EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 2, 3));
606 }
607 }
608
609 TEST_F(CardStackLayoutManagerTest, ScrollCollapseExpansionOfLargeStack) {
610 BOOL boolValues[2] = {NO, YES};
611 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
612 const unsigned int kCardCount = 10;
613 base::scoped_nsobject<CardStackLayoutManager> stack(
614 newStackOfNCards(kCardCount, boolValues[i]));
615
616 const float kEndLimit =
617 kDefaultEndLimitFraction * [stack fannedStackLength];
618 [stack setEndLimit:kEndLimit];
619
620 [stack fanOutCardsWithStartIndex:0];
621 ValidateCardPositioningConstraints(stack, kEndLimit, true);
622 EXPECT_TRUE([stack stackIsFullyFannedOut]);
623 EXPECT_FALSE([stack stackIsFullyCollapsed]);
624 EXPECT_FALSE([stack stackIsFullyOverextended]);
625
626 // Test fanning out/overextension toward end stack.
627 [stack scrollCardAtIndex:0
628 byDelta:-10
629 allowEarlyOverscroll:NO
630 decayOnOverscroll:YES
631 scrollLeadingCards:YES];
632 ValidateCardPositioningConstraints(stack, kEndLimit, true);
633 EXPECT_FALSE([stack stackIsFullyFannedOut]);
634 EXPECT_FALSE([stack stackIsFullyCollapsed]);
635 EXPECT_FALSE([stack stackIsFullyOverextended]);
636
637 [stack scrollCardAtIndex:0
638 byDelta:[stack maximumStackLength]
639 allowEarlyOverscroll:NO
640 decayOnOverscroll:YES
641 scrollLeadingCards:YES];
642 ValidateCardPositioningConstraints(stack, kEndLimit, true);
643 EXPECT_TRUE([stack stackIsFullyFannedOut]);
644 EXPECT_FALSE([stack stackIsFullyCollapsed]);
645 EXPECT_TRUE([stack stackIsFullyOverextended]);
646
647 // Test collapsing/overextension toward start stack.
648 [stack fanOutCardsWithStartIndex:0];
649 [stack scrollCardAtIndex:0
650 byDelta:-2.0 * [stack maximumStackLength]
651 allowEarlyOverscroll:NO
652 decayOnOverscroll:YES
653 scrollLeadingCards:YES];
654 ValidateCardPositioningConstraints(stack, kEndLimit, true);
655 EXPECT_FALSE([stack stackIsFullyFannedOut]);
656 EXPECT_TRUE([stack stackIsFullyCollapsed]);
657 EXPECT_TRUE([stack stackIsFullyOverextended]);
658 }
659 }
660
661 TEST_F(CardStackLayoutManagerTest, ScrollCollapseExpansionOfStackCornerCases) {
662 BOOL boolValues[2] = {NO, YES};
663 const unsigned int kCardCount = 1;
664 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
665 base::scoped_nsobject<CardStackLayoutManager> stack(
666 newStackOfNCards(kCardCount, boolValues[i]));
667
668 // A laid-out stack with one card is fully collapsed and fully fanned out,
669 // but not fully overextended.
670 [stack fanOutCardsWithStartIndex:0];
671 EXPECT_TRUE([stack stackIsFullyFannedOut]);
672 EXPECT_TRUE([stack stackIsFullyCollapsed]);
673 EXPECT_FALSE([stack stackIsFullyOverextended]);
674
675 // A stack with no cards is fully collapsed, fully fanned out, and fully
676 // overextended.
677 [stack removeCard:[[stack cards] objectAtIndex:0]];
678 EXPECT_TRUE([stack stackIsFullyFannedOut]);
679 EXPECT_TRUE([stack stackIsFullyCollapsed]);
680 EXPECT_TRUE([stack stackIsFullyOverextended]);
681 }
682 }
683
684 TEST_F(CardStackLayoutManagerTest, OneCardOverscroll) {
685 BOOL boolValues[2] = {NO, YES};
686 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
687 const unsigned int kCardCount = 1;
688 const float kScrollAwayAmount = 20.0;
689 base::scoped_nsobject<CardStackLayoutManager> stack(
690 newStackOfNCards(kCardCount, boolValues[i]));
691 StackCard* firstCard = [[stack cards] objectAtIndex:0];
692
693 const float kEndLimit =
694 kDefaultEndLimitFraction * [stack fannedStackLength];
695 [stack setEndLimit:kEndLimit];
696 [stack fanOutCardsWithStartIndex:0];
697 ValidateCardPositioningConstraints(stack, kEndLimit, true);
698 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
699
700 // Scrolling toward end should result in overscroll.
701 [stack scrollCardAtIndex:0
702 byDelta:kScrollAwayAmount
703 allowEarlyOverscroll:YES
704 decayOnOverscroll:YES
705 scrollLeadingCards:YES];
706 ValidateCardPositioningConstraints(stack, kEndLimit, true);
707 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
708 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
709 EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
710 LayoutOffset(stack, firstCard.layout.position));
711
712 // Eliminate the overscroll to test scrolling toward start.
713 [stack eliminateOverextension];
714 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
715 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
716 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
717
718 // Scrolling toward start should result in overscroll.
719 [stack scrollCardAtIndex:0
720 byDelta:-kScrollAwayAmount
721 allowEarlyOverscroll:YES
722 decayOnOverscroll:YES
723 scrollLeadingCards:YES];
724 ValidateCardPositioningConstraints(stack, kEndLimit, true);
725 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
726 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
727 EXPECT_FLOAT_EQ(kMargin - kScrollAwayAmount,
728 LayoutOffset(stack, firstCard.layout.position));
729 }
730 }
731
732 TEST_F(CardStackLayoutManagerTest, MaximumOverextensionAmount) {
733 BOOL boolValues[2] = {NO, YES};
734 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
735 const unsigned int kCardCount = 1;
736 const float kScrollAwayAmount = 20.0;
737 base::scoped_nsobject<CardStackLayoutManager> stack(
738 newStackOfNCards(kCardCount, boolValues[i]));
739 StackCard* firstCard = [[stack cards] objectAtIndex:0];
740
741 const float kEndLimit =
742 kDefaultEndLimitFraction * [stack fannedStackLength];
743 [stack setEndLimit:kEndLimit];
744 [stack fanOutCardsWithStartIndex:0];
745 ValidateCardPositioningConstraints(stack, kEndLimit, true);
746 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
747
748 // Scrolling toward start/end should have no impact.
749 [stack setMaximumOverextensionAmount:0];
750 [stack scrollCardAtIndex:0
751 byDelta:kScrollAwayAmount
752 allowEarlyOverscroll:YES
753 decayOnOverscroll:YES
754 scrollLeadingCards:YES];
755 ValidateCardPositioningConstraints(stack, kEndLimit, true);
756 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
757 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
758 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
759 EXPECT_FLOAT_EQ(0, [stack overextensionAmount]);
760
761 [stack scrollCardAtIndex:0
762 byDelta:-kScrollAwayAmount
763 allowEarlyOverscroll:YES
764 decayOnOverscroll:YES
765 scrollLeadingCards:YES];
766 ValidateCardPositioningConstraints(stack, kEndLimit, true);
767 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
768 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
769 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
770 EXPECT_FLOAT_EQ(0, [stack overextensionAmount]);
771
772 // Setting a maximum overextension amount > 0 should allow overscrolling to
773 // that limit.
774 CGFloat maxOverextensionAmount = kScrollAwayAmount / 2.0;
775 [stack setMaximumOverextensionAmount:maxOverextensionAmount];
776 [stack scrollCardAtIndex:0
777 byDelta:kScrollAwayAmount
778 allowEarlyOverscroll:YES
779 decayOnOverscroll:NO
780 scrollLeadingCards:YES];
781 ValidateCardPositioningConstraints(stack, kEndLimit, true);
782 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
783 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
784 EXPECT_FLOAT_EQ(kMargin + maxOverextensionAmount,
785 LayoutOffset(stack, firstCard.layout.position));
786 EXPECT_FLOAT_EQ(maxOverextensionAmount, [stack overextensionAmount]);
787
788 // Eliminate the overscroll to test scrolling toward start.
789 [stack eliminateOverextension];
790
791 [stack scrollCardAtIndex:0
792 byDelta:-kScrollAwayAmount
793 allowEarlyOverscroll:YES
794 decayOnOverscroll:NO
795 scrollLeadingCards:YES];
796 ValidateCardPositioningConstraints(stack, kEndLimit, true);
797 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
798 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
799 EXPECT_FLOAT_EQ(kMargin - maxOverextensionAmount,
800 LayoutOffset(stack, firstCard.layout.position));
801 EXPECT_FLOAT_EQ(maxOverextensionAmount, [stack overextensionAmount]);
802 }
803 }
804
805 TEST_F(CardStackLayoutManagerTest, DecayOnOverscroll) {
806 BOOL boolValues[2] = {NO, YES};
807 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
808 const unsigned int kCardCount = 1;
809 const float kScrollAwayAmount = 10.0;
810 base::scoped_nsobject<CardStackLayoutManager> stack(
811 newStackOfNCards(kCardCount, boolValues[i]));
812 StackCard* firstCard = [[stack cards] objectAtIndex:0];
813
814 const float kEndLimit =
815 kDefaultEndLimitFraction * [stack fannedStackLength];
816 [stack setEndLimit:kEndLimit];
817 [stack fanOutCardsWithStartIndex:0];
818 ValidateCardPositioningConstraints(stack, kEndLimit, true);
819 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
820
821 // Scrolling toward end by |kScrollAwayAmount| should result in overscroll.
822 [stack scrollCardAtIndex:0
823 byDelta:kScrollAwayAmount
824 allowEarlyOverscroll:YES
825 decayOnOverscroll:YES
826 scrollLeadingCards:YES];
827 ValidateCardPositioningConstraints(stack, kEndLimit, true);
828 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
829 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
830 EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
831 LayoutOffset(stack, firstCard.layout.position));
832
833 // Scrolling again by |kScrollAwayAmount| with no decay on overscroll
834 // should result in another move of |kScrollAwayAmount|.
835 [stack scrollCardAtIndex:0
836 byDelta:kScrollAwayAmount
837 allowEarlyOverscroll:YES
838 decayOnOverscroll:NO
839 scrollLeadingCards:YES];
840 ValidateCardPositioningConstraints(stack, kEndLimit, true);
841 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
842 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
843 EXPECT_FLOAT_EQ(kMargin + 2 * kScrollAwayAmount,
844 LayoutOffset(stack, firstCard.layout.position));
845
846 // Scrolling by |kScrollAwayAmount| a third time *with* decay on overscroll
847 // should result in a move of less than |kScrollAwayAmount|.
848 [stack scrollCardAtIndex:0
849 byDelta:kScrollAwayAmount
850 allowEarlyOverscroll:YES
851 decayOnOverscroll:YES
852 scrollLeadingCards:YES];
853 ValidateCardPositioningConstraints(stack, kEndLimit, true);
854 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
855 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
856 EXPECT_GT(kMargin + 3 * kScrollAwayAmount,
857 LayoutOffset(stack, firstCard.layout.position));
858 }
859 }
860
861 TEST_F(CardStackLayoutManagerTest, EliminateOverextension) {
862 BOOL boolValues[2] = {NO, YES};
863 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
864 const unsigned int kCardCount = 2;
865 const float kScrollAwayAmount = 20.0;
866 base::scoped_nsobject<CardStackLayoutManager> stack(
867 newStackOfNCards(kCardCount, boolValues[i]));
868 StackCard* firstCard = [[stack cards] objectAtIndex:0];
869 StackCard* secondCard = [[stack cards] objectAtIndex:1];
870
871 const float kEndLimit =
872 kDefaultEndLimitFraction * [stack fannedStackLength];
873 [stack setEndLimit:kEndLimit];
874 [stack fanOutCardsWithStartIndex:0];
875 ValidateCardPositioningConstraints(stack, kEndLimit, true);
876 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
877
878 CGFloat firstCardInitialOrigin =
879 LayoutOffset(stack, firstCard.layout.position);
880 CGFloat secondCardInitialOrigin =
881 LayoutOffset(stack, secondCard.layout.position);
882
883 // Scrolling toward end should result in overscroll.
884 [stack scrollCardAtIndex:0
885 byDelta:kScrollAwayAmount
886 allowEarlyOverscroll:YES
887 decayOnOverscroll:YES
888 scrollLeadingCards:YES];
889 ValidateCardPositioningConstraints(stack, kEndLimit, true);
890 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
891 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
892 EXPECT_FLOAT_EQ(firstCardInitialOrigin + kScrollAwayAmount,
893 LayoutOffset(stack, firstCard.layout.position));
894 EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
895 LayoutOffset(stack, secondCard.layout.position));
896
897 // Calling |eliminateOverextension| should undo the overscroll on the first
898 // and second card.
899 [stack eliminateOverextension];
900 EXPECT_FLOAT_EQ(firstCardInitialOrigin,
901 LayoutOffset(stack, firstCard.layout.position));
902 EXPECT_FLOAT_EQ(secondCardInitialOrigin,
903 LayoutOffset(stack, secondCard.layout.position));
904
905 // Scrolling toward start should result in overscroll.
906 [stack scrollCardAtIndex:0
907 byDelta:-kScrollAwayAmount
908 allowEarlyOverscroll:YES
909 decayOnOverscroll:YES
910 scrollLeadingCards:YES];
911 ValidateCardPositioningConstraints(stack, kEndLimit, true);
912 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
913 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
914 EXPECT_FLOAT_EQ(firstCardInitialOrigin - kScrollAwayAmount,
915 LayoutOffset(stack, firstCard.layout.position));
916 EXPECT_FLOAT_EQ(secondCardInitialOrigin - kScrollAwayAmount,
917 LayoutOffset(stack, secondCard.layout.position));
918
919 // Calling |eliminateOverextension| should undo the overscroll on the first
920 // card but leave the second card as-is, since it's not overscrolled.
921 [stack eliminateOverextension];
922 EXPECT_FLOAT_EQ(firstCardInitialOrigin,
923 LayoutOffset(stack, firstCard.layout.position));
924 EXPECT_FLOAT_EQ(secondCardInitialOrigin - kScrollAwayAmount,
925 LayoutOffset(stack, secondCard.layout.position));
926
927 // Reset state to test pinch.
928 [stack fanOutCardsWithStartIndex:0];
929
930 // Pinching first card toward end should result in it being overextended.
931 [stack handleMultitouchWithFirstDelta:kScrollAwayAmount
932 secondDelta:kScrollAwayAmount
933 firstCardIndex:0
934 secondCardIndex:1
935 decayOnOverpinch:YES];
936 EXPECT_FLOAT_EQ(firstCardInitialOrigin + kScrollAwayAmount,
937 LayoutOffset(stack, firstCard.layout.position));
938 EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
939 LayoutOffset(stack, secondCard.layout.position));
940 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
941 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
942
943 // ELiminating the overextension, which is now an overpinch, should restore
944 // the first card to its initial position but not alter the offset of the
945 // second card.
946 [stack eliminateOverextension];
947 EXPECT_FLOAT_EQ(firstCardInitialOrigin,
948 LayoutOffset(stack, firstCard.layout.position));
949 EXPECT_FLOAT_EQ(secondCardInitialOrigin + kScrollAwayAmount,
950 LayoutOffset(stack, secondCard.layout.position));
951 }
952 }
953
954 TEST_F(CardStackLayoutManagerTest, MultiCardOverscroll) {
955 BOOL boolValues[2] = {NO, YES};
956 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
957 const unsigned int kCardCount = 3;
958 const float kScrollAwayAmount = 100.0;
959 base::scoped_nsobject<CardStackLayoutManager> stack(
960 newStackOfNCards(kCardCount, boolValues[i]));
961 StackCard* firstCard = [[stack cards] objectAtIndex:0];
962
963 const float kEndLimit =
964 kDefaultEndLimitFraction * [stack fannedStackLength];
965 [stack setEndLimit:kEndLimit];
966 [stack fanOutCardsWithStartIndex:0];
967 ValidateCardPositioningConstraints(stack, kEndLimit, true);
968 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
969 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
970 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
971 // Scrolling away from the start stack should result in overscroll.
972 [stack scrollCardAtIndex:0
973 byDelta:kScrollAwayAmount
974 allowEarlyOverscroll:YES
975 decayOnOverscroll:YES
976 scrollLeadingCards:YES];
977 ValidateCardPositioningConstraints(stack, kEndLimit, true);
978 EXPECT_TRUE([stack overextensionTowardEndOnFirstCard]);
979 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
980 EXPECT_FLOAT_EQ(kMargin + kScrollAwayAmount,
981 LayoutOffset(stack, firstCard.layout.position));
982
983 // Calling |eliminateOverextension| should restore the stack to its previous
984 // fanned-out state.
985 [stack eliminateOverextension];
986 ValidateCardPositioningConstraints(stack, kEndLimit, true);
987 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
988 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
989 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
990
991 // Scrolling toward the start more than is necessary to fully collapse the
992 // stack should result in overscroll toward the start.
993 CGFloat lastCardCollapsedPosition =
994 kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
995 StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
996 CGFloat distanceToCollapsedStack =
997 lastCardCollapsedPosition -
998 LayoutOffset(stack, lastCard.layout.position);
999 [stack scrollCardAtIndex:kCardCount - 1
1000 byDelta:distanceToCollapsedStack - 10
1001 allowEarlyOverscroll:YES
1002 decayOnOverscroll:YES
1003 scrollLeadingCards:YES];
1004 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1005 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
1006 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
1007 EXPECT_FLOAT_EQ(kMargin - 10,
1008 LayoutOffset(stack, firstCard.layout.position));
1009 }
1010 }
1011
1012 TEST_F(CardStackLayoutManagerTest, Fling) {
1013 BOOL boolValues[2] = {NO, YES};
1014 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1015 const unsigned int kCardCount = 3;
1016 base::scoped_nsobject<CardStackLayoutManager> stack(
1017 newStackOfNCards(kCardCount, boolValues[i]));
1018 StackCard* firstCard = [[stack cards] objectAtIndex:0];
1019
1020 const float kEndLimit =
1021 kDefaultEndLimitFraction * [stack fannedStackLength];
1022 [stack setEndLimit:kEndLimit];
1023 [stack fanOutCardsWithStartIndex:0];
1024 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1025 EXPECT_FLOAT_EQ(kMargin, LayoutOffset(stack, firstCard.layout.position));
1026 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1027 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1028
1029 // Flinging on the first card should not result in overscroll...
1030 [stack scrollCardAtIndex:0
1031 byDelta:-20.0
1032 allowEarlyOverscroll:NO
1033 decayOnOverscroll:YES
1034 scrollLeadingCards:YES];
1035 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1036 EXPECT_FALSE([stack overextensionTowardStartOnCardAtIndex:0]);
1037 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
1038 // ... until the last card becomes overscrolled.
1039 [stack scrollCardAtIndex:0
1040 byDelta:-[stack maximumStackLength]
1041 allowEarlyOverscroll:NO
1042 decayOnOverscroll:YES
1043 scrollLeadingCards:YES];
1044 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1045 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
1046 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
1047 }
1048 }
1049
1050 TEST_F(CardStackLayoutManagerTest, ScrollAroundStartStack) {
1051 BOOL boolValues[2] = {NO, YES};
1052 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1053 const unsigned int kCardCount = 3;
1054 base::scoped_nsobject<CardStackLayoutManager> stack(
1055 newStackOfNCards(kCardCount, boolValues[i]));
1056
1057 const float kEndLimit =
1058 kDefaultEndLimitFraction * [stack fannedStackLength];
1059 [stack setEndLimit:kEndLimit];
1060 [stack fanOutCardsWithStartIndex:0];
1061
1062 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1063 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
1064 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1065
1066 // Scroll second card into start stack.
1067 CGFloat cardTwoStartStackOffset =
1068 kMargin + [stack staggerOffsetForIndexFromEdge:1];
1069 LayoutRectPosition cardTwoPosition =
1070 ((StackCard*)[[stack cards] objectAtIndex:1]).layout.position;
1071 [stack scrollCardAtIndex:1
1072 byDelta:cardTwoStartStackOffset -
1073 LayoutOffset(stack, cardTwoPosition)
1074 allowEarlyOverscroll:YES
1075 decayOnOverscroll:YES
1076 scrollLeadingCards:YES];
1077 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1078 EXPECT_EQ(1, [stack lastStartStackCardIndex]);
1079 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1080
1081 // Scroll third card toward start stack, and check that everything is as
1082 // expected.
1083 [stack scrollCardAtIndex:2
1084 byDelta:kMaxStagger / -2.0
1085 allowEarlyOverscroll:YES
1086 decayOnOverscroll:YES
1087 scrollLeadingCards:YES];
1088 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1089 EXPECT_EQ(1, [stack lastStartStackCardIndex]);
1090 EXPECT_FLOAT_EQ(kMaxStagger / 2.0, SeparationOnLayoutAxis(stack, 1, 2));
1091
1092 // Scroll third card away from stack stack, and check that second card
1093 // doesn't come out before it should.
1094 [stack scrollCardAtIndex:2
1095 byDelta:kMaxStagger / 4.0
1096 allowEarlyOverscroll:YES
1097 decayOnOverscroll:YES
1098 scrollLeadingCards:YES];
1099 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1100 EXPECT_EQ(1, [stack lastStartStackCardIndex]);
1101 EXPECT_FLOAT_EQ(kMaxStagger * .75, SeparationOnLayoutAxis(stack, 1, 2));
1102 [stack scrollCardAtIndex:2
1103 byDelta:kMaxStagger / 4.0 + 1
1104 allowEarlyOverscroll:YES
1105 decayOnOverscroll:YES
1106 scrollLeadingCards:YES];
1107 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1108 EXPECT_EQ(0, [stack lastStartStackCardIndex]);
1109 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1110 }
1111 }
1112
1113 TEST_F(CardStackLayoutManagerTest, ScrollAroundEndStack) {
1114 BOOL boolValues[2] = {NO, YES};
1115 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1116 const unsigned int kCardCount = 7;
1117 base::scoped_nsobject<CardStackLayoutManager> stack(
1118 newStackOfNCards(kCardCount, boolValues[i]));
1119
1120 const float kEndLimit = 0.2 * [stack fannedStackLength];
1121 [stack setEndLimit:kEndLimit];
1122 [stack fanOutCardsWithStartIndex:4];
1123
1124 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
1125 EXPECT_EQ(4, [stack lastStartStackCardIndex]);
1126 EXPECT_EQ(7, [stack firstEndStackCardIndex]);
1127 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1128
1129 // Scroll seventh card into end stack.
1130 CGFloat cardSevenEndStackOffset =
1131 kEndLimit - ([stack staggerOffsetForIndexFromEdge:0] +
1132 [stack minStackStaggerAmount]);
1133 LayoutRectPosition cardSevenPosition =
1134 ((StackCard*)[[stack cards] objectAtIndex:6]).layout.position;
1135 [stack scrollCardAtIndex:6
1136 byDelta:cardSevenEndStackOffset -
1137 LayoutOffset(stack, cardSevenPosition)
1138 allowEarlyOverscroll:YES
1139 decayOnOverscroll:YES
1140 scrollLeadingCards:YES];
1141 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1142 EXPECT_EQ([stack firstEndStackCardIndex], 6);
1143 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
1144
1145 // Scroll sixth card toward end stack, and check that everything is as
1146 // expected.
1147 [stack scrollCardAtIndex:5
1148 byDelta:kMaxStagger / 2.0
1149 allowEarlyOverscroll:YES
1150 decayOnOverscroll:YES
1151 scrollLeadingCards:YES];
1152 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1153 EXPECT_EQ([stack firstEndStackCardIndex], 6);
1154 EXPECT_FLOAT_EQ(kMaxStagger / 2.0, SeparationOnLayoutAxis(stack, 5, 6));
1155
1156 // Scroll sixth card away from end stack, and check that seventh card
1157 // doesn't come out before it should.
1158 [stack scrollCardAtIndex:5
1159 byDelta:kMaxStagger / -4.0
1160 allowEarlyOverscroll:YES
1161 decayOnOverscroll:YES
1162 scrollLeadingCards:YES];
1163 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1164 EXPECT_EQ([stack firstEndStackCardIndex], 6);
1165 EXPECT_FLOAT_EQ(SeparationOnLayoutAxis(stack, 5, 6), kMaxStagger * .75);
1166 [stack scrollCardAtIndex:5
1167 byDelta:kMaxStagger / -4.0 - 1
1168 allowEarlyOverscroll:YES
1169 decayOnOverscroll:YES
1170 scrollLeadingCards:YES];
1171 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1172 EXPECT_EQ([stack firstEndStackCardIndex], 7);
1173 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
1174 }
1175 }
1176
1177 TEST_F(CardStackLayoutManagerTest, BasicMultitouch) {
1178 BOOL boolValues[2] = {NO, YES};
1179 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1180 const unsigned int kCardCount = 3;
1181 base::scoped_nsobject<CardStackLayoutManager> stack(
1182 newStackOfNCards(kCardCount, boolValues[i]));
1183
1184 const float kEndLimit =
1185 kDefaultEndLimitFraction * [stack fannedStackLength];
1186 [stack setEndLimit:kEndLimit];
1187 [stack fanOutCardsWithStartIndex:0];
1188
1189 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1190 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1191 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1192
1193 [stack handleMultitouchWithFirstDelta:-10
1194 secondDelta:50
1195 firstCardIndex:1
1196 secondCardIndex:2
1197 decayOnOverpinch:YES];
1198 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1199 EXPECT_FLOAT_EQ(kMaxStagger - 10, SeparationOnLayoutAxis(stack, 0, 1));
1200 EXPECT_FLOAT_EQ(kMaxStagger + 60, SeparationOnLayoutAxis(stack, 1, 2));
1201 }
1202 }
1203
1204 TEST_F(CardStackLayoutManagerTest, MultitouchBoundedByNeighbor) {
1205 BOOL boolValues[2] = {NO, YES};
1206 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1207 const unsigned int kCardCount = 3;
1208 base::scoped_nsobject<CardStackLayoutManager> stack(
1209 newStackOfNCards(kCardCount, boolValues[i]));
1210
1211 // Make sure that the stack end limit isn't hit in this test.
1212 const float kEndLimit = 2.0 * [stack maximumStackLength];
1213 [stack setEndLimit:kEndLimit];
1214 [stack fanOutCardsWithStartIndex:0];
1215
1216 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1217 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1218 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1219
1220 CGFloat cardSize = (i > 0) ? kCardHeight : kCardWidth;
1221
1222 // Verify that it's not possible to pinch the third card entirely off the
1223 // second card.
1224 [stack handleMultitouchWithFirstDelta:0
1225 secondDelta:2.0 * cardSize
1226 firstCardIndex:1
1227 secondCardIndex:2
1228 decayOnOverpinch:YES];
1229 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1230 // Verify that it's not possible to pinch the second card entirely off the
1231 // third card.
1232 [stack handleMultitouchWithFirstDelta:-2.0 * cardSize
1233 secondDelta:0
1234 firstCardIndex:1
1235 secondCardIndex:2
1236 decayOnOverpinch:YES];
1237 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1238 // Verify that it's not possible to pinch the two cards off each other.
1239 [stack handleMultitouchWithFirstDelta:-cardSize
1240 secondDelta:cardSize
1241 firstCardIndex:1
1242 secondCardIndex:2
1243 decayOnOverpinch:YES];
1244 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1245 }
1246 }
1247
1248 TEST_F(CardStackLayoutManagerTest, OverpinchTowardStart) {
1249 BOOL boolValues[2] = {NO, YES};
1250 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1251 const unsigned int kCardCount = 2;
1252 base::scoped_nsobject<CardStackLayoutManager> stack(
1253 newStackOfNCards(kCardCount, boolValues[i]));
1254
1255 const float kEndLimit =
1256 kDefaultEndLimitFraction * [stack fannedStackLength];
1257 [stack setEndLimit:kEndLimit];
1258 [stack fanOutCardsWithStartIndex:0];
1259
1260 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1261 StackCard* firstCard = [[stack cards] objectAtIndex:0];
1262 LayoutRectPosition firstCardPosition = firstCard.layout.position;
1263 StackCard* secondCard = [[stack cards] objectAtIndex:1];
1264 LayoutRectPosition secondCardPosition = secondCard.layout.position;
1265
1266 [stack handleMultitouchWithFirstDelta:-20
1267 secondDelta:10
1268 firstCardIndex:0
1269 secondCardIndex:1
1270 decayOnOverpinch:YES];
1271 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1272 // First card should have been overpinched.
1273 EXPECT_TRUE([stack overextensionTowardStartOnCardAtIndex:0]);
1274 EXPECT_FALSE([stack overextensionTowardEndOnFirstCard]);
1275 EXPECT_FLOAT_EQ(LayoutOffset(stack, firstCardPosition) - 20,
1276 LayoutOffset(stack, firstCard.layout.position));
1277 EXPECT_FLOAT_EQ(LayoutOffset(stack, secondCardPosition) + 10,
1278 LayoutOffset(stack, secondCard.layout.position));
1279 }
1280 }
1281
1282 TEST_F(CardStackLayoutManagerTest, OverpinchTowardEnd) {
1283 BOOL boolValues[2] = {NO, YES};
1284 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1285 const unsigned int kCardCount = 2;
1286 base::scoped_nsobject<CardStackLayoutManager> stack(
1287 newStackOfNCards(kCardCount, boolValues[i]));
1288
1289 const float kEndLimit =
1290 kDefaultEndLimitFraction * [stack fannedStackLength];
1291 [stack setEndLimit:kEndLimit];
1292 [stack fanOutCardsWithStartIndex:0];
1293
1294 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1295 StackCard* firstCard = [[stack cards] objectAtIndex:0];
1296 LayoutRectPosition firstCardPosition = firstCard.layout.position;
1297 StackCard* secondCard = [[stack cards] objectAtIndex:1];
1298 LayoutRectPosition secondCardPosition = secondCard.layout.position;
1299
1300 [stack handleMultitouchWithFirstDelta:20
1301 secondDelta:10
1302 firstCardIndex:0
1303 secondCardIndex:1
1304 decayOnOverpinch:YES];
1305 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1306 // Both first and second card should have moved.
1307 EXPECT_FLOAT_EQ(LayoutOffset(stack, firstCardPosition) + 20,
1308 LayoutOffset(stack, firstCard.layout.position));
1309 EXPECT_FLOAT_EQ(LayoutOffset(stack, secondCardPosition) + 10,
1310 LayoutOffset(stack, secondCard.layout.position));
1311 }
1312 }
1313
1314 TEST_F(CardStackLayoutManagerTest, StressMultitouch) {
1315 BOOL boolValues[2] = {NO, YES};
1316 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1317 const unsigned int kCardCount = 30;
1318 base::scoped_nsobject<CardStackLayoutManager> stack(
1319 newStackOfNCards(kCardCount, boolValues[i]));
1320
1321 const float kEndLimit =
1322 kDefaultEndLimitFraction * [stack fannedStackLength];
1323 [stack setEndLimit:kEndLimit];
1324 [stack fanOutCardsWithStartIndex:0];
1325
1326 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1327
1328 [stack handleMultitouchWithFirstDelta:-10
1329 secondDelta:50
1330 firstCardIndex:5
1331 secondCardIndex:10
1332 decayOnOverpinch:YES];
1333 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1334
1335 [stack handleMultitouchWithFirstDelta:20
1336 secondDelta:-10
1337 firstCardIndex:3
1338 secondCardIndex:15
1339 decayOnOverpinch:YES];
1340 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1341
1342 [stack handleMultitouchWithFirstDelta:-20
1343 secondDelta:-10
1344 firstCardIndex:0
1345 secondCardIndex:4
1346 decayOnOverpinch:YES];
1347 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1348 }
1349 }
1350
1351 TEST_F(CardStackLayoutManagerTest, ScrollAfterMultitouch) {
1352 BOOL boolValues[2] = {NO, YES};
1353 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1354 const unsigned int kCardCount = 3;
1355 base::scoped_nsobject<CardStackLayoutManager> stack(
1356 newStackOfNCards(kCardCount, boolValues[i]));
1357
1358 const float kEndLimit =
1359 kDefaultEndLimitFraction * [stack fannedStackLength];
1360 const float kPinchDistance = 50;
1361 [stack setEndLimit:kEndLimit];
1362 [stack fanOutCardsWithStartIndex:0];
1363
1364 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1365 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1366
1367 [stack handleMultitouchWithFirstDelta:0
1368 secondDelta:kPinchDistance
1369 firstCardIndex:1
1370 secondCardIndex:2
1371 decayOnOverpinch:YES];
1372 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1373 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1374 SeparationOnLayoutAxis(stack, 1, 2));
1375
1376 [stack scrollCardAtIndex:2
1377 byDelta:10
1378 allowEarlyOverscroll:YES
1379 decayOnOverscroll:YES
1380 scrollLeadingCards:YES];
1381 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1382 // Separation between cards should be maintained.
1383 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1384 SeparationOnLayoutAxis(stack, 1, 2));
1385 [stack scrollCardAtIndex:2
1386 byDelta:-20
1387 allowEarlyOverscroll:YES
1388 decayOnOverscroll:YES
1389 scrollLeadingCards:YES];
1390 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1391 // Separation between cards should be maintained.
1392 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1393 SeparationOnLayoutAxis(stack, 1, 2));
1394 }
1395 }
1396
1397 TEST_F(CardStackLayoutManagerTest, ScrollEveningOutAfterMultitouch) {
1398 BOOL boolValues[2] = {NO, YES};
1399 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1400 const unsigned int kCardCount = 3;
1401 base::scoped_nsobject<CardStackLayoutManager> stack(
1402 newStackOfNCards(kCardCount, boolValues[i]));
1403
1404 const float kEndLimit =
1405 kDefaultEndLimitFraction * [stack fannedStackLength];
1406 const float kPinchDistance = kMaxStagger / 2.0;
1407 [stack setEndLimit:kEndLimit];
1408 [stack fanOutCardsWithStartIndex:0];
1409
1410 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1411 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1412
1413 // Scroll cards toward start stack to give some room to scroll second card
1414 // toward end stack (see below).
1415 [stack scrollCardAtIndex:1
1416 byDelta:-10
1417 allowEarlyOverscroll:YES
1418 decayOnOverscroll:YES
1419 scrollLeadingCards:YES];
1420 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1421 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1422
1423 // Pinch the third card closer to the second card.
1424 [stack handleMultitouchWithFirstDelta:0
1425 secondDelta:-kPinchDistance
1426 firstCardIndex:1
1427 secondCardIndex:2
1428 decayOnOverpinch:YES];
1429 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1430 EXPECT_FLOAT_EQ(kMaxStagger - kPinchDistance,
1431 SeparationOnLayoutAxis(stack, 1, 2));
1432
1433 // Separation between cards should be maintained when second card is
1434 // scrolled towards third card.
1435 [stack scrollCardAtIndex:1
1436 byDelta:10
1437 allowEarlyOverscroll:YES
1438 decayOnOverscroll:YES
1439 scrollLeadingCards:YES];
1440 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1441 EXPECT_FLOAT_EQ(kMaxStagger - kPinchDistance,
1442 SeparationOnLayoutAxis(stack, 1, 2));
1443
1444 // Scrolling second card away from third card by the distance that the
1445 // third card was pinched should restore separation of |kMaxStagger|
1446 // between the second and third card.
1447 [stack scrollCardAtIndex:1
1448 byDelta:-kPinchDistance
1449 allowEarlyOverscroll:YES
1450 decayOnOverscroll:YES
1451 scrollLeadingCards:YES];
1452 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1453 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1454 }
1455 }
1456
1457 TEST_F(CardStackLayoutManagerTest, ScrollAroundStartStackAfterMultitouch) {
1458 BOOL boolValues[2] = {NO, YES};
1459 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1460 const unsigned int kCardCount = 3;
1461 base::scoped_nsobject<CardStackLayoutManager> stack(
1462 newStackOfNCards(kCardCount, boolValues[i]));
1463
1464 const float kEndLimit =
1465 kDefaultEndLimitFraction * [stack fannedStackLength];
1466 const float kPinchDistance = 50;
1467 [stack setEndLimit:kEndLimit];
1468 [stack fanOutCardsWithStartIndex:0];
1469
1470 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1471 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1472
1473 [stack handleMultitouchWithFirstDelta:0
1474 secondDelta:kPinchDistance
1475 firstCardIndex:1
1476 secondCardIndex:2
1477 decayOnOverpinch:YES];
1478 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1479 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1480 SeparationOnLayoutAxis(stack, 1, 2));
1481
1482 [stack scrollCardAtIndex:2
1483 byDelta:-20
1484 allowEarlyOverscroll:YES
1485 decayOnOverscroll:YES
1486 scrollLeadingCards:YES];
1487 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1488 // Separation between cards should be maintained.
1489 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1490 SeparationOnLayoutAxis(stack, 1, 2));
1491
1492 // Scroll the cards completely into the start stack.
1493 CGFloat lastCardCollapsedPosition =
1494 kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
1495 StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
1496 CGFloat distanceToCollapsedStack =
1497 lastCardCollapsedPosition -
1498 LayoutOffset(stack, lastCard.layout.position);
1499 [stack scrollCardAtIndex:2
1500 byDelta:distanceToCollapsedStack
1501 allowEarlyOverscroll:YES
1502 decayOnOverscroll:YES
1503 scrollLeadingCards:YES];
1504 EXPECT_EQ((NSInteger)(kCardCount - 1), [stack lastStartStackCardIndex]);
1505 // Scroll the cards out of the start stack: they should now be separated by
1506 // |kMaxStagger|.
1507 [stack scrollCardAtIndex:2
1508 byDelta:2 * kMaxStagger
1509 allowEarlyOverscroll:YES
1510 decayOnOverscroll:YES
1511 scrollLeadingCards:YES];
1512 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 1, 2));
1513 }
1514 }
1515
1516 TEST_F(CardStackLayoutManagerTest, ScrollAroundEndStackAfterMultitouch) {
1517 BOOL boolValues[2] = {NO, YES};
1518 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1519 const unsigned int kCardCount = 7;
1520 base::scoped_nsobject<CardStackLayoutManager> stack(
1521 newStackOfNCards(kCardCount, boolValues[i]));
1522
1523 const float kEndLimit = 0.3 * [stack fannedStackLength];
1524 const float kPinchDistance = 20;
1525 [stack setEndLimit:kEndLimit];
1526 // Start in the middle of the stack to be able to scroll cards into the end
1527 // stack.
1528 [stack fanOutCardsWithStartIndex:4];
1529
1530 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
1531 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1532
1533 [stack handleMultitouchWithFirstDelta:0
1534 secondDelta:kPinchDistance
1535 firstCardIndex:5
1536 secondCardIndex:6
1537 decayOnOverpinch:YES];
1538 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1539 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1540 SeparationOnLayoutAxis(stack, 5, 6));
1541
1542 [stack scrollCardAtIndex:5
1543 byDelta:-10
1544 allowEarlyOverscroll:YES
1545 decayOnOverscroll:YES
1546 scrollLeadingCards:YES];
1547 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1548 // Separation between cards should be maintained.
1549 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1550 SeparationOnLayoutAxis(stack, 5, 6));
1551 // Scroll the two cards in question into the end stack.
1552 CGFloat cardSixEndStackPosition =
1553 kEndLimit - [stack staggerOffsetForIndexFromEdge:1];
1554 LayoutRectPosition cardSixPosition =
1555 ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
1556 [stack scrollCardAtIndex:5
1557 byDelta:cardSixEndStackPosition -
1558 LayoutOffset(stack, cardSixPosition)
1559 allowEarlyOverscroll:YES
1560 decayOnOverscroll:YES
1561 scrollLeadingCards:YES];
1562 cardSixPosition =
1563 ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
1564 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1565 EXPECT_EQ(5, [stack firstEndStackCardIndex]);
1566 // Scroll the cards out of the end stack: cards should now be
1567 // separated by |kMaxStagger|.
1568 [stack scrollCardAtIndex:5
1569 byDelta:-2.0 * kMaxStagger
1570 allowEarlyOverscroll:YES
1571 decayOnOverscroll:YES
1572 scrollLeadingCards:YES];
1573 EXPECT_FLOAT_EQ(SeparationOnLayoutAxis(stack, 5, 6), kMaxStagger);
1574 }
1575 }
1576
1577 TEST_F(CardStackLayoutManagerTest, ScrollAfterPinchOutOfStartStack) {
1578 BOOL boolValues[2] = {NO, YES};
1579 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1580 const unsigned int kCardCount = 3;
1581 base::scoped_nsobject<CardStackLayoutManager> stack(
1582 newStackOfNCards(kCardCount, boolValues[i]));
1583
1584 const float kEndLimit =
1585 kDefaultEndLimitFraction * [stack fannedStackLength];
1586 const float kPinchDistance = 50;
1587 [stack setEndLimit:kEndLimit];
1588 [stack fanOutCardsWithStartIndex:0];
1589
1590 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 0, 1));
1591 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1592
1593 [stack handleMultitouchWithFirstDelta:0
1594 secondDelta:kPinchDistance
1595 firstCardIndex:1
1596 secondCardIndex:2
1597 decayOnOverpinch:YES];
1598 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1599 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1600 SeparationOnLayoutAxis(stack, 1, 2));
1601
1602 [stack scrollCardAtIndex:1
1603 byDelta:-20
1604 allowEarlyOverscroll:YES
1605 decayOnOverscroll:YES
1606 scrollLeadingCards:YES];
1607 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1608 // Separation between cards should be maintained.
1609 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1610 SeparationOnLayoutAxis(stack, 1, 2));
1611 // Scroll the cards completely into the start stack.
1612 CGFloat lastCardCollapsedPosition =
1613 kMargin + [stack staggerOffsetForIndexFromEdge:kCardCount - 1];
1614 StackCard* lastCard = [[stack cards] objectAtIndex:kCardCount - 1];
1615 CGFloat distanceToCollapsedStack =
1616 lastCardCollapsedPosition -
1617 LayoutOffset(stack, lastCard.layout.position);
1618 [stack scrollCardAtIndex:kCardCount - 1
1619 byDelta:distanceToCollapsedStack
1620 allowEarlyOverscroll:YES
1621 decayOnOverscroll:YES
1622 scrollLeadingCards:YES];
1623 EXPECT_EQ((NSInteger)(kCardCount - 1), [stack lastStartStackCardIndex]);
1624 CGFloat inStackSeparation = SeparationOnLayoutAxis(stack, 1, 2);
1625 // Pinch the third card far out of the start stack.
1626 [stack handleMultitouchWithFirstDelta:0
1627 secondDelta:kMaxStagger
1628 firstCardIndex:1
1629 secondCardIndex:2
1630 decayOnOverpinch:YES];
1631 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1632 EXPECT_FLOAT_EQ(inStackSeparation + kMaxStagger,
1633 SeparationOnLayoutAxis(stack, 1, 2));
1634 // A scroll should immediately bring the second card out of the start
1635 // stack, without affecting the distance between the second and third cards.
1636 [stack scrollCardAtIndex:2
1637 byDelta:kMaxStagger / 2.0
1638 allowEarlyOverscroll:YES
1639 decayOnOverscroll:YES
1640 scrollLeadingCards:YES];
1641 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1642 EXPECT_FLOAT_EQ(inStackSeparation + kMaxStagger,
1643 SeparationOnLayoutAxis(stack, 1, 2));
1644 }
1645 }
1646
1647 TEST_F(CardStackLayoutManagerTest, ScrollAfterPinchOutOfEndStack) {
1648 BOOL boolValues[2] = {NO, YES};
1649 for (unsigned long i = 0; i < arraysize(boolValues); i++) {
1650 const unsigned int kCardCount = 7;
1651 base::scoped_nsobject<CardStackLayoutManager> stack(
1652 newStackOfNCards(kCardCount, boolValues[i]));
1653
1654 const float kEndLimit = 0.3 * [stack fannedStackLength];
1655 const float kPinchDistance = 20;
1656 [stack setEndLimit:kEndLimit];
1657 // Start in the middle of the stack to be able to scroll cards into the end
1658 // stack.
1659 [stack fanOutCardsWithStartIndex:4];
1660
1661 EXPECT_FLOAT_EQ(kMaxStagger, SeparationOnLayoutAxis(stack, 5, 6));
1662 ValidateCardPositioningConstraints(stack, kEndLimit, true);
1663
1664 [stack handleMultitouchWithFirstDelta:0
1665 secondDelta:kPinchDistance
1666 firstCardIndex:5
1667 secondCardIndex:6
1668 decayOnOverpinch:YES];
1669 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1670 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1671 SeparationOnLayoutAxis(stack, 5, 6));
1672
1673 [stack scrollCardAtIndex:5
1674 byDelta:-10
1675 allowEarlyOverscroll:YES
1676 decayOnOverscroll:YES
1677 scrollLeadingCards:YES];
1678 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1679 // Separation between cards should be maintained.
1680 EXPECT_FLOAT_EQ(kMaxStagger + kPinchDistance,
1681 SeparationOnLayoutAxis(stack, 5, 6));
1682
1683 // Scroll the two cards in question into the end stack.
1684 CGFloat cardSixEndStackOffset =
1685 kEndLimit - ([stack staggerOffsetForIndexFromEdge:1] +
1686 [stack minStackStaggerAmount]);
1687 LayoutRectPosition cardSixPosition =
1688 ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
1689 [stack scrollCardAtIndex:5
1690 byDelta:cardSixEndStackOffset -
1691 LayoutOffset(stack, cardSixPosition)
1692 allowEarlyOverscroll:YES
1693 decayOnOverscroll:YES
1694 scrollLeadingCards:YES];
1695 cardSixPosition =
1696 ((StackCard*)[[stack cards] objectAtIndex:5]).layout.position;
1697 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1698 EXPECT_EQ(5, [stack firstEndStackCardIndex]);
1699 CGFloat inStackSeparation = SeparationOnLayoutAxis(stack, 5, 6);
1700 // Pinch the sixth card far out of the start stack.
1701 [stack handleMultitouchWithFirstDelta:-2.0 * kMaxStagger
1702 secondDelta:0
1703 firstCardIndex:5
1704 secondCardIndex:6
1705 decayOnOverpinch:YES];
1706 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1707 EXPECT_EQ(6, [stack firstEndStackCardIndex]);
1708 EXPECT_FLOAT_EQ(inStackSeparation + 2 * kMaxStagger,
1709 SeparationOnLayoutAxis(stack, 5, 6));
1710 // A scroll should immediately bring the seventh card out of the start
1711 // stack, without affecting the distance between the sixth and seventh
1712 // cards.
1713 [stack scrollCardAtIndex:5
1714 byDelta:kMaxStagger / -2.0
1715 allowEarlyOverscroll:YES
1716 decayOnOverscroll:YES
1717 scrollLeadingCards:YES];
1718 ValidateCardPositioningConstraints(stack, kEndLimit, false);
1719 EXPECT_EQ(7, [stack firstEndStackCardIndex]);
1720 EXPECT_FLOAT_EQ(inStackSeparation + 2 * kMaxStagger,
1721 SeparationOnLayoutAxis(stack, 5, 6));
1722 }
1723 }
1724
1725 } // namespace
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698