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

Side by Side Diff: ios/chrome/browser/ui/stack_view/card_stack_layout_manager.mm

Issue 2587023002: Upstream Chrome on iOS source code [8/11]. (Closed)
Patch Set: Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View 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 #import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h"
6
7 #include <algorithm>
8 #include <cmath>
9
10 #include "base/logging.h"
11 #include "ios/chrome/browser/ui/rtl_geometry.h"
12 #import "ios/chrome/browser/ui/stack_view/stack_card.h"
13 #import "ios/chrome/browser/ui/ui_util.h"
14
15 namespace {
16
17 // The maximum number of cards that should be staggered at a collapse point.
18 const NSInteger kMaxVisibleStaggerCount = 4;
19 // The amount that each of the staggered cards in a stack should be staggered
20 // when fully collapsed.
21 const CGFloat kMinStackStaggerAmount = 4.0;
22 // The amount that a card should overlap with a previous/subsequent card when
23 // it is extended the maximum distance away (e.g., after a multitouch event).
24 const CGFloat kFullyExtendedCardOverlap = 8.0;
25 // The amount that a card's position is allowed to drift toward overextension
26 // before the card is considered to be overextended (i.e., an epsilon to allow
27 // for floating-point imprecision).
28 const CGFloat kDistanceBeforeOverextension = 0.0001;
29 // The factor by which scroll is decayed on overscroll.
30 const CGFloat kOverextensionDecayFactor = 2.0;
31 // The amount by which a card is scrolled when asked to scroll it away from its
32 // preceding neighbor.
33 const CGFloat kScrollAwayFromNeighborAmount = 200;
34
35 } // namespace
36
37 @interface CardStackLayoutManager ()
38
39 // Exposes |kMinStackStaggerAmount| for tests.
40 - (CGFloat)minStackStaggerAmount;
41 // Exposes |kScrollAwayFromNeighborAmount| for tests.
42 - (CGFloat)scrollCardAwayFromNeighborAmount;
43 // Returns the current start stack limit allowing for overextension as follows:
44 // - If the card at |index| is not overextended toward the start, returns
45 // |startLimit_|.
46 // - Otherwise, returns the value of the start limit such that the position of
47 // the card at |index| in the start stack is its current position (with the
48 // exception that the value is capped at |limitOfOverextensionTowardStart|).
49 - (CGFloat)startStackLimitAllowingForOverextensionOnCardAtIndex:
50 (NSUInteger)index;
51 // Based on cards' current positions, |startLimit|, and |endLimit_|, caps cards
52 // that should be in the start and end stack. The reason that |startLimit| is
53 // a parameter is that the position of the start stack can change due to
54 // overextension.
55 - (void)layOutEdgeStacksWithStartLimit:(CGFloat)startLimit;
56 // Based on cards' current positions and |limit|, caps cards that should be in
57 // the start stack. The reason that |limit| is a parameter is that the desired
58 // position for the visual start of the start stack can change due to
59 // overextension.
60 - (void)layOutStartStackWithLimit:(CGFloat)limit;
61 // Positions the cards in the end stack based on |endLimit_|, leaving enough
62 // margin so that the last card in the stack has |kMinStackStaggerAmount|
63 // amount of visibility before |endLimit_|.
64 - (void)layOutEndStack;
65 // Computes the index of what should be the the inner boundary card in the
66 // indicated stack based on the current positions of the cards and the desired
67 // |visualStackLimit|.
68 - (NSInteger)computeEdgeStackBoundaryIndex:(BOOL)startStack
69 withVisualStackLimit:(CGFloat)visualStackLimit;
70 // Computes what the origin of the inner boundary card in the indicated stack
71 // based on |visualStackLimit|.
72 - (CGFloat)computeEdgeStackInnerEdge:(BOOL)startStack
73 withVisualStackLimit:(CGFloat)visualStackLimit;
74 // Fans out the cards in the end stack and then recalculates the end stack.
75 - (void)recomputeEndStack;
76 // Fans out the cards in the start stack/end stack to be |maxStagger_| away
77 // from each other, with the first card in the stack being the greater of
78 // |maxStagger_| and its current distance away from its neighboring non-
79 // collapsed card.
80 - (void)fanOutCardsInEdgeStack:(BOOL)startStack;
81 // Returns the distance separating the origin of the card at |firstIndex| from
82 // that of the card at |secondIndex|.
83 - (CGFloat)distanceBetweenCardAtIndex:(NSUInteger)firstIndex
84 andCardAtIndex:(NSUInteger)secondIndex;
85 // Returns the minimum offset that the first card is allowed to over-extend to
86 // toward the start.
87 - (CGFloat)limitOfOverextensionTowardStart;
88 // Returns the maximum offset that the first card is allowed to overscroll to
89 // toward the end.
90 - (CGFloat)limitOfOverscrollTowardEnd;
91 // Caps overscroll toward start and end to maximum allowed amounts and re-lays
92 // out the start and end stacks. If |allowEarlyOverscroll| is |YES|,
93 // overscrolling is allowed to occur naturally on the scrolled card; otherwise,
94 // overscrolling is not allowed to occur until the stack is fully
95 // collapsed/fanned out.
96 - (void)capOverscrollWithScrolledIndex:(NSUInteger)scrolledIndex
97 allowEarlyOverscroll:(BOOL)allowEarlyOverscroll;
98 // Caps overscroll toward end to maximum allowed amount.
99 - (void)capOverscrollTowardEnd;
100 // Moves the cards so that any overscroll is eliminated.
101 - (void)eliminateOverscroll;
102 // Moves the cards so that any overpinch is eliminated.
103 - (void)eliminateOverpinch;
104 // Returns the maximum amount that a card can be offset from a
105 // preceding/following card: |cardSize - kFullyExtendedCardOverlap|.
106 - (CGFloat)maximumCardSeparation;
107 // Returns the maximum offset that the card at |index| can have given the
108 // constraint that no card can start more than
109 // |maximumCardSeparation:| away from the previous card.
110 - (CGFloat)maximumOffsetForCardAtIndex:(NSInteger)index;
111 // Returns the offset that the card at |index| would have after calling
112 // |fanOutCardsWithStartIndex:0|.
113 - (CGFloat)cappedFanoutOffsetForCardAtIndex:(NSInteger)index;
114 // Moves the card at |index| by |amount| along the layout axis, centered in the
115 // other direction at layoutAxisPosition_.
116 - (void)moveCardAtIndex:(NSUInteger)index byAmount:(CGFloat)amount;
117 // Moves |card|'s layout by |amount| along the layout axis.
118 - (void)moveCard:(StackCard*)card byAmount:(CGFloat)amount;
119 // Moves each of the cards between |startIndex| and |endIndex| inclusive by
120 // |delta| along the layout axis.
121 - (void)moveCardsFromIndex:(NSUInteger)startIndex
122 toIndex:(NSUInteger)endIndex
123 byAmount:(CGFloat)amount;
124
125 // Moves each of the cards before/after |index| (as indicated by |toEnd|)
126 // by |amount| with the constraint that for a non-edge-stack card (and for
127 // cards in the start stack if |restoreFanOutInStartStack| is |YES|), the
128 // amount that the card is moved is decreased by the amount necessary to
129 // restore the separation between that card and its next/previous neighbor to
130 // |maxStagger_|. Assumes that the card at |index| has been moved by |amount|
131 // prior to calling this method. `
132 - (void)moveCardsrestoringFanoutFromIndex:(NSUInteger)index
133 toEnd:(BOOL)toEnd
134 byAmount:(CGFloat)amount
135 restoreFanOutInStartStack:(BOOL)restoreFanOutInStartStack;
136 // Moves the origin of the card at |index| to |offset| along the layout axis,
137 // centered in the other direction at layoutAxisPosition_.
138 - (void)moveOriginOfCardAtIndex:(NSUInteger)index toOffset:(CGFloat)offset;
139 // Returns |offset| modified as necessary to make sure that it is not too
140 // close or too far from the origin of its constraining neighbor (previous or
141 // next, as determined by |constrainingNeighborIsPrevious|).
142 - (CGFloat)constrainedOffset:(CGFloat)offset
143 forCardAtIndex:(NSInteger)index
144 constrainingNeighborIsPrevious:(BOOL)isPrevious;
145 // Moves the cards starting at |index| by an amount that decays from
146 // |drivingDelta| with each card that gets moved.
147 - (void)moveCardsStartingAtIndex:(NSInteger)index
148 towardsEnd:(BOOL)towardsEnd
149 withDrivingDelta:(CGFloat)delta;
150 // Moves the cards in-between |firstIndex| and |secondIndex > firstIndex|
151 // inclusive via a proportional blend of |firstDelta| and |secondDelta|.
152 - (void)blendOffsetsOfCardsBetweenFirstIndex:(NSInteger)firstIndex
153 secondIndex:(NSInteger)secondIndex
154 withFirstDelta:(CGFloat)firstDelta
155 secondDelta:(CGFloat)secondDelta;
156 // Returns the length of |size| in the current layout direction.
157 - (CGFloat)layoutLength:(CGSize)size;
158 // Returns the offset of |position| in the current layout direction.
159 - (CGFloat)layoutOffset:(LayoutRectPosition)position;
160 // Returns the offset of |card| in the current layout direction.
161 - (CGFloat)cardOffsetOnLayoutAxis:(StackCard*)card;
162 // Returns the pixel offset relative to the first/last card in a fully
163 // compressed stack to show a card that is |countFromEdge| fram the start/end.
164 - (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge;
165 // Returns the pixel offset relative to the first/last card in a fully
166 // compressed stack where a card being pushed onto the stack should start
167 // moving the existing cards.
168 - (CGFloat)pushThresholdForIndexFromEdge:(NSInteger)countFromEdge;
169 // Controls whether the cards keep their views synchronized when updates are
170 // made to their frame/bounds/center.
171 - (void)setSynchronizeCardViews:(BOOL)synchronizeViews;
172 // Returns YES if |index| is in the start stack.
173 - (BOOL)isInStartStack:(NSUInteger)index;
174 // Returns YES if |index| is in the end stack.
175 - (BOOL)isInEndStack:(NSUInteger)index;
176 // Returns YES if |index| is in the start or end stack.
177 - (BOOL)isInEdgeStack:(NSUInteger)index;
178
179 @end
180
181 #pragma mark -
182
183 @implementation CardStackLayoutManager
184
185 @synthesize cardSize = cardSize_;
186 @synthesize maxStagger = maxStagger_;
187 @synthesize maximumOverextensionAmount = maximumOverextensionAmount_;
188 @synthesize endLimit = endLimit_;
189 @synthesize layoutAxisPosition = layoutAxisPosition_;
190 @synthesize startLimit = startLimit_;
191 @synthesize layoutIsVertical = layoutIsVertical_;
192 @synthesize lastStartStackCardIndex = lastStartStackCardIndex_;
193 @synthesize firstEndStackCardIndex = firstEndStackCardIndex_;
194
195 - (id)init {
196 if ((self = [super init])) {
197 cards_.reset([[NSMutableArray alloc] init]);
198 layoutIsVertical_ = YES;
199 lastStartStackCardIndex_ = -1;
200 firstEndStackCardIndex_ = -1;
201 }
202 return self;
203 }
204
205 - (CGFloat)minStackStaggerAmount {
206 return kMinStackStaggerAmount;
207 }
208
209 - (CGFloat)scrollCardAwayFromNeighborAmount {
210 return kScrollAwayFromNeighborAmount;
211 }
212
213 - (void)setEndLimit:(CGFloat)endLimit {
214 endLimit_ = endLimit;
215 [self recomputeEndStack];
216 }
217
218 - (void)addCard:(StackCard*)card {
219 [self insertCard:card atIndex:[cards_ count]];
220 }
221
222 - (void)insertCard:(StackCard*)card atIndex:(NSUInteger)index {
223 card.size = cardSize_;
224 [cards_ insertObject:card atIndex:index];
225 }
226
227 - (void)removeCard:(StackCard*)card {
228 // Update edge stack boundary indices if necessary.
229 NSInteger cardIndex = [cards_ indexOfObject:card];
230 DCHECK(cardIndex != NSNotFound);
231 if (cardIndex <= lastStartStackCardIndex_)
232 lastStartStackCardIndex_ -= 1;
233 if (cardIndex < firstEndStackCardIndex_)
234 firstEndStackCardIndex_ -= 1;
235
236 [cards_ removeObject:card];
237 }
238
239 - (void)removeAllCards {
240 lastStartStackCardIndex_ = -1;
241 firstEndStackCardIndex_ = -1;
242 [cards_ removeAllObjects];
243 }
244
245 - (void)setCardSize:(CGSize)size {
246 cardSize_ = size;
247 NSUInteger i = 0;
248 CGFloat previousFirstCardOffset = 0;
249 CGFloat newFirstCardOffset = 0;
250 for (StackCard* card in cards_.get()) {
251 CGFloat offset = [self cardOffsetOnLayoutAxis:card];
252 card.size = cardSize_;
253 CGFloat newOffset = offset;
254
255 // Attempt to preserve card positions, but ensure that the deck starts
256 // within overextension limits and that all cards not in the start stack are
257 // within minimum/maximum separation limits of their preceding neighbors.
258 if (i == 0) {
259 newOffset = std::max(newOffset, [self limitOfOverextensionTowardStart]);
260 newOffset = std::min(newOffset, [self limitOfOverscrollTowardEnd]);
261 previousFirstCardOffset = offset;
262 newFirstCardOffset = newOffset;
263 } else if ((NSInteger)i <= lastStartStackCardIndex_) {
264 // Preserve the layout of the start stack.
265 newOffset = newFirstCardOffset + (offset - previousFirstCardOffset);
266 } else {
267 newOffset = [self constrainedOffset:newOffset
268 forCardAtIndex:i
269 constrainingNeighborIsPrevious:YES];
270 }
271
272 [self moveOriginOfCardAtIndex:i toOffset:newOffset];
273 i++;
274 }
275 }
276
277 - (void)setLayoutIsVertical:(BOOL)layoutIsVertical {
278 if (layoutIsVertical_ == layoutIsVertical)
279 return;
280 layoutIsVertical_ = layoutIsVertical;
281 // Restore the cards' positions along the new layout axis.
282 for (NSUInteger i = 0; i < [cards_ count]; i++) {
283 LayoutRectPosition position = [[cards_ objectAtIndex:i] layout].position;
284 CGFloat prevLayoutAxisOffset =
285 layoutIsVertical_ ? position.leading : position.originY;
286 [self moveOriginOfCardAtIndex:i toOffset:prevLayoutAxisOffset];
287 }
288 }
289
290 - (void)setLayoutAxisPosition:(CGFloat)position {
291 layoutAxisPosition_ = position;
292 for (StackCard* card in cards_.get()) {
293 LayoutRect layout = card.layout;
294 if (layoutIsVertical_)
295 layout.position.leading = position - 0.5 * layout.size.width;
296 else
297 layout.position.originY = position - 0.5 * layout.size.height;
298 card.layout = layout;
299 }
300 }
301
302 - (NSArray*)cards {
303 return cards_;
304 }
305
306 - (void)fanOutCardsWithStartIndex:(NSUInteger)startIndex {
307 NSUInteger numCards = [cards_ count];
308 if (numCards == 0)
309 return;
310 DCHECK(startIndex < numCards);
311
312 // Temporarily turn off updates to the cards' views as this method might be
313 // being called from within an animation, and updating the coordinates of a
314 // |UIView| multiple times while it is animating can cause undesired
315 // behavior.
316 [self setSynchronizeCardViews:NO];
317
318 // Move the cards starting at |startIndex| into place.
319 for (NSUInteger i = 0; i < numCards - startIndex; ++i) {
320 // The start cap for this card, accounting for visual stacking.
321 CGFloat uncappedPosition = i * maxStagger_ + startLimit_;
322 [self moveOriginOfCardAtIndex:(startIndex + i) toOffset:uncappedPosition];
323 }
324
325 // Fan out the cards behind the one at |startIndex|.
326 for (NSInteger i = (startIndex - 1); i >= 0; --i) {
327 CGFloat uncappedPosition = startLimit_ - (startIndex - i) * maxStagger_;
328 [self moveOriginOfCardAtIndex:i toOffset:uncappedPosition];
329 }
330
331 [self layOutEdgeStacksWithStartLimit:startLimit_];
332 [self setSynchronizeCardViews:YES];
333 }
334
335 - (void)recomputeEndStack {
336 [self setSynchronizeCardViews:NO];
337 if (firstEndStackCardIndex_ != -1)
338 [self fanOutCardsInEdgeStack:NO];
339 [self layOutEndStack];
340 [self setSynchronizeCardViews:YES];
341 }
342
343 // Starts the fan at the stack boundary if the neighboring non-collapsed card
344 // is at least |maxStagger_| away from the stack (note that due to pinching,
345 // the neighboring card can be an arbitrary distance away from the stack);
346 // otherwise, starts the fan at |maxStagger_| away from that neighboring
347 // non-collapsed card.
348 - (void)fanOutCardsInEdgeStack:(BOOL)startStack {
349 NSUInteger numCards = [cards_ count];
350 if (numCards == 0)
351 return;
352 NSUInteger numCardsToMove;
353 if (startStack)
354 numCardsToMove = lastStartStackCardIndex_ + 1;
355 else
356 numCardsToMove = numCards - firstEndStackCardIndex_;
357
358 if (numCardsToMove == 0)
359 return;
360
361 // Find the offset at which to start.
362 NSUInteger stackBoundaryIndex =
363 startStack ? lastStartStackCardIndex_ : firstEndStackCardIndex_;
364 CGFloat startOffset =
365 [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:stackBoundaryIndex]];
366 if ((startStack && stackBoundaryIndex < numCards - 1) ||
367 (!startStack && stackBoundaryIndex > 0)) {
368 // Ensure that the stack is laid out starting at least |maxStagger_|
369 // separation from the neighboring non-collapsed card.
370 NSUInteger nonCollapsedLimitIndex =
371 startStack ? stackBoundaryIndex + 1 : stackBoundaryIndex - 1;
372 CGFloat nonCollapsedLimitOffset = [self
373 cardOffsetOnLayoutAxis:[cards_ objectAtIndex:nonCollapsedLimitIndex]];
374 CGFloat distance = fabs(nonCollapsedLimitOffset - startOffset);
375 if (distance < maxStagger_) {
376 startOffset = startStack ? nonCollapsedLimitOffset - maxStagger_
377 : nonCollapsedLimitOffset + maxStagger_;
378 }
379 }
380
381 NSUInteger currentIndex = stackBoundaryIndex;
382 for (NSUInteger i = 0; i < numCardsToMove; i++) {
383 DCHECK(currentIndex < numCards);
384 CGFloat delta = startStack ? i * -maxStagger_ : i * maxStagger_;
385 CGFloat newOrigin = startOffset + delta;
386 [self moveOriginOfCardAtIndex:currentIndex toOffset:newOrigin];
387 currentIndex = startStack ? currentIndex - 1 : currentIndex + 1;
388 }
389 }
390
391 - (CGFloat)distanceBetweenCardAtIndex:(NSUInteger)firstIndex
392 andCardAtIndex:(NSUInteger)secondIndex {
393 DCHECK(firstIndex < [cards_ count]);
394 DCHECK(secondIndex < [cards_ count]);
395 CGFloat firstOrigin =
396 [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:firstIndex]];
397 CGFloat secondOrigin =
398 [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:secondIndex]];
399 return std::abs(secondOrigin - firstOrigin);
400 }
401
402 - (BOOL)overextensionTowardStartOnCardAtIndex:(NSUInteger)index {
403 DCHECK(index < [cards_ count]);
404 CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]];
405 CGFloat collapsedOffset =
406 startLimit_ + [self staggerOffsetForIndexFromEdge:index];
407 // Uses an epsilon to allow for floating-point imprecision.
408 return (offset < collapsedOffset - kDistanceBeforeOverextension);
409 }
410
411 - (BOOL)overextensionTowardEndOnFirstCard {
412 if ([cards_ count] == 0)
413 return NO;
414 CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]];
415 // Uses an epsilon to allow for floating-point imprecision.
416 return (offset > startLimit_ + kDistanceBeforeOverextension);
417 }
418
419 - (CGFloat)limitOfOverextensionTowardStart {
420 return startLimit_ - maximumOverextensionAmount_;
421 }
422
423 - (CGFloat)limitOfOverscrollTowardEnd {
424 return startLimit_ + maximumOverextensionAmount_;
425 }
426
427 - (void)capOverscrollWithScrolledIndex:(NSUInteger)scrolledIndex
428 allowEarlyOverscroll:(BOOL)allowEarlyOverscroll {
429 DCHECK(scrolledIndex < [cards_ count]);
430 [self capOverscrollTowardEnd];
431 // Allow for overscroll as appropriate when laying out the start stack.
432 NSUInteger allowedStartOverscrollIndex =
433 allowEarlyOverscroll ? scrolledIndex : [cards_ count] - 1;
434 CGFloat startLimit =
435 [self startStackLimitAllowingForOverextensionOnCardAtIndex:
436 allowedStartOverscrollIndex];
437 [self layOutEdgeStacksWithStartLimit:startLimit];
438 }
439
440 // Reduces overscroll on the first card to its maximum allowed amount, and
441 // undoes the effect of the extra overscroll on the rest of the cards. NOTE: In
442 // the current implementation of scroll, undoing the effect of the extra
443 // overscroll on the rest of the cards is as simple as moving them the reverse
444 // of the extra overscroll amount. If the implementation of scroll becomes more
445 // complex, undoing the effect of the extra overscroll may have to become more
446 // complex to correspond.
447 - (void)capOverscrollTowardEnd {
448 if ([cards_ count] == 0)
449 return;
450 CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]];
451 CGFloat distance = firstCardOffset - [self limitOfOverscrollTowardEnd];
452 if (distance > 0)
453 [self moveCardsFromIndex:0 toIndex:[cards_ count] - 1 byAmount:-distance];
454 }
455
456 - (void)eliminateOverextension {
457 if (treatOverExtensionAsScroll_)
458 [self eliminateOverscroll];
459 else
460 [self eliminateOverpinch];
461 }
462
463 // If eliminating overscroll that was toward the end (where cards have
464 // overscrolled into the end stack), the cards scroll so that cards fan out
465 // from the end stack properly. If eliminating overscroll from the start, the
466 // overscrolled cards simply move back into place.
467 - (void)eliminateOverscroll {
468 if ([cards_ count] == 0)
469 return;
470 [self setSynchronizeCardViews:NO];
471 CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]];
472 CGFloat overscrollEliminationAmount = startLimit_ - firstCardOffset;
473 if (overscrollEliminationAmount <= 0) {
474 [self scrollCardAtIndex:0
475 byDelta:overscrollEliminationAmount
476 allowEarlyOverscroll:YES
477 decayOnOverscroll:NO
478 scrollLeadingCards:YES];
479 }
480 [self layOutEdgeStacksWithStartLimit:startLimit_];
481 [self setSynchronizeCardViews:YES];
482 }
483
484 - (void)eliminateOverpinch {
485 if ([cards_ count] == 0)
486 return;
487 DCHECK(previousFirstPinchCardIndex_ != NSNotFound);
488 DCHECK(previousSecondPinchCardIndex_ != NSNotFound);
489 DCHECK(previousFirstPinchCardIndex_ < [cards_ count]);
490 DCHECK(previousSecondPinchCardIndex_ < [cards_ count]);
491 CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]];
492 CGFloat overpinchReductionAmount = startLimit_ - firstCardOffset;
493 if (overpinchReductionAmount >= 0) {
494 // Overpinching was toward the start stack. The overpinched cards simply
495 // move back into place.
496 [self layOutStartStackWithLimit:startLimit_];
497 } else {
498 // Overpinching was toward the end stack. The effect of the overpinch is
499 // undone by a corresponding negating pinch.
500 [self handleMultitouchWithFirstDelta:overpinchReductionAmount
501 secondDelta:0
502 firstCardIndex:previousFirstPinchCardIndex_
503 secondCardIndex:previousSecondPinchCardIndex_
504 decayOnOverpinch:NO];
505 }
506 [self setSynchronizeCardViews:NO];
507 [self setSynchronizeCardViews:YES];
508 }
509
510 - (void)scrollCardAtIndex:(NSUInteger)index
511 byDelta:(CGFloat)delta
512 allowEarlyOverscroll:(BOOL)allowEarlyOverscroll
513 decayOnOverscroll:(BOOL)decayOnOverscroll
514 scrollLeadingCards:(BOOL)scrollLeadingCards {
515 NSUInteger numCards = [cards_ count];
516 if (numCards == 0)
517 return;
518 DCHECK(index < [cards_ count]);
519
520 treatOverExtensionAsScroll_ = YES;
521
522 // Temporarily turn off updates to the cards' views as this method might be
523 // being called from within an animation, and updating the coordinates of a
524 // |UIView| multiple times while it is animating can cause undesired
525 // behavior.
526 [self setSynchronizeCardViews:NO];
527 BOOL scrollIsTowardsEnd = (delta > 0);
528
529 if (decayOnOverscroll) {
530 // NOTE: This calculation is imprecise around the boundary case of a scroll
531 // that moves the stack from not being overscrolled to being overscrolled.
532 // This imprecision does not present a problem in practice, and eliminates
533 // the need to compute the distance until the stack becomes overscrolled,
534 // which is an unfortunately fiddly computation.
535 if ([self overextensionTowardStartOnCardAtIndex:0] ||
536 [self overextensionTowardEndOnFirstCard])
537 delta = delta / kOverextensionDecayFactor;
538 }
539
540 NSUInteger leadingIndex = index;
541 if (scrollLeadingCards)
542 leadingIndex = scrollIsTowardsEnd ? numCards - 1 : 0;
543
544 // Move the scrolled card and those further on in the direction being
545 // scrolled by |delta|.
546 if (scrollIsTowardsEnd)
547 [self moveCardsFromIndex:index toIndex:leadingIndex byAmount:delta];
548 else
549 [self moveCardsFromIndex:leadingIndex toIndex:index byAmount:delta];
550
551 // Move the cards trailing the scrolled card, but restore fan out in the
552 // process as necessary.
553 [self moveCardsrestoringFanoutFromIndex:index
554 toEnd:!scrollIsTowardsEnd
555 byAmount:delta
556 restoreFanOutInStartStack:allowEarlyOverscroll];
557
558 [self capOverscrollWithScrolledIndex:index
559 allowEarlyOverscroll:allowEarlyOverscroll];
560 [self setSynchronizeCardViews:YES];
561 }
562
563 - (void)scrollCardAtIndex:(NSUInteger)index awayFromNeighbor:(BOOL)preceding {
564 DCHECK(index < [cards_ count]);
565 if (index == 0)
566 return;
567
568 CGFloat currentOffset =
569 [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]];
570 CGFloat offsetToScrollTo =
571 preceding ? currentOffset + kScrollAwayFromNeighborAmount
572 : currentOffset - kScrollAwayFromNeighborAmount;
573
574 CGFloat limitOffsetToScrollTo;
575 if (index == [cards_ count] - 1 && !preceding) {
576 limitOffsetToScrollTo = endLimit_ - [self maximumCardSeparation];
577 } else {
578 CGFloat neighborOffset =
579 preceding
580 ? [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index - 1]]
581 : [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index + 1]];
582 limitOffsetToScrollTo = preceding
583 ? neighborOffset + [self maximumCardSeparation]
584 : neighborOffset - [self maximumCardSeparation];
585 }
586 offsetToScrollTo = preceding
587 ? std::min(offsetToScrollTo, limitOffsetToScrollTo)
588 : std::max(offsetToScrollTo, limitOffsetToScrollTo);
589
590 CGFloat distanceToScroll = offsetToScrollTo - currentOffset;
591
592 [self setSynchronizeCardViews:NO];
593 if (preceding) {
594 [self moveCardsFromIndex:index
595 toIndex:[cards_ count] - 1
596 byAmount:distanceToScroll];
597 } else {
598 [self moveCardsFromIndex:0 toIndex:index byAmount:distanceToScroll];
599 }
600 [self layOutEdgeStacksWithStartLimit:startLimit_];
601 [self setSynchronizeCardViews:YES];
602 }
603
604 - (void)moveCardsrestoringFanoutFromIndex:(NSUInteger)index
605 toEnd:(BOOL)toEnd
606 byAmount:(CGFloat)amount
607 restoreFanOutInStartStack:(BOOL)restoreFanOutInStartStack {
608 DCHECK(index < [cards_ count]);
609
610 // This method assumes that the cards are being moved toward the card at
611 // |index|.
612 if (toEnd)
613 DCHECK(amount <= 0);
614 else
615 DCHECK(amount >= 0);
616
617 CGFloat currentAmount = amount;
618 // The index of the card against which separation will be checked for the
619 // card currently being moved.
620 NSUInteger precedingIndex = index;
621 // The index of the card currently being moved.
622 NSUInteger currentIndex = toEnd ? precedingIndex + 1 : precedingIndex - 1;
623 NSInteger step = toEnd ? 1 : -1;
624
625 // Move all the cards after/before the one at |index| as indicated by |toEnd|.
626 NSInteger numCardsToMove = toEnd ? ([cards_ count] - index - 1) : index;
627 for (int i = 0; i < numCardsToMove; i++) {
628 BOOL restoreFanout = YES;
629 // Do not restore fanout when cards are moving into an edge stack unless
630 // directed to.
631 if (toEnd) {
632 if (!restoreFanOutInStartStack) {
633 restoreFanout = (![self isInStartStack:currentIndex] &&
634 ![self isInStartStack:precedingIndex]);
635 }
636 } else {
637 restoreFanout = (![self isInEndStack:currentIndex] &&
638 ![self isInEndStack:precedingIndex]);
639 }
640
641 if (restoreFanout) {
642 CGFloat distance = [self distanceBetweenCardAtIndex:currentIndex
643 andCardAtIndex:precedingIndex];
644 // Account for the fact that the card at |precedingIndex| has already
645 // been moved.
646 distance -= std::abs(currentAmount);
647 // Calculate how much of the move (if any) should be eliminated in order
648 // to restore fan out between this card and the preceding card.
649 CGFloat amountToRestoreFanOut =
650 std::max((CGFloat)0, maxStagger_ - distance);
651 if (amountToRestoreFanOut > std::abs(currentAmount))
652 currentAmount = 0;
653 else if (currentAmount > 0)
654 currentAmount -= amountToRestoreFanOut;
655 else
656 currentAmount += amountToRestoreFanOut;
657 }
658 [self moveCardAtIndex:currentIndex byAmount:currentAmount];
659 precedingIndex = currentIndex;
660 currentIndex += step;
661 }
662 }
663
664 - (CGFloat)clipDelta:(CGFloat)delta forCardAtIndex:(NSInteger)index {
665 DCHECK(index < (NSInteger)[cards_ count]);
666 StackCard* card = [cards_ objectAtIndex:index];
667 CGFloat startingOffset = [self cardOffsetOnLayoutAxis:card];
668 if (delta < 0) {
669 // |delta| is towards start stack.
670 CGFloat collapsedPosition =
671 startLimit_ + [self staggerOffsetForIndexFromEdge:index];
672 delta = std::max(delta, collapsedPosition - startingOffset);
673 } else {
674 // |delta| is towards end stack.
675 NSInteger indexFromEnd = [cards_ count] - 1 - index;
676 CGFloat collapsedPosition =
677 endLimit_ - kMinStackStaggerAmount -
678 [self staggerOffsetForIndexFromEdge:indexFromEnd];
679 delta = std::min(delta, collapsedPosition - startingOffset);
680 }
681 return delta;
682 }
683
684 - (CGFloat)maximumCardSeparation {
685 return [self layoutLength:self.cardSize] - kFullyExtendedCardOverlap;
686 }
687
688 - (CGFloat)maximumOffsetForCardAtIndex:(NSInteger)index {
689 DCHECK(index < (NSInteger)[cards_ count]);
690 // Account for the fact that the first card may be overextended toward the
691 // start or the end.
692 CGFloat firstCardOffset = [self cardOffsetOnLayoutAxis:[cards_ firstObject]];
693 return firstCardOffset + index * [self maximumCardSeparation];
694 }
695
696 - (CGFloat)cappedFanoutOffsetForCardAtIndex:(NSInteger)index {
697 CGFloat fannedOutPosition = startLimit_ + index * maxStagger_;
698 NSInteger indexFromEnd = [cards_ count] - 1 - index;
699 CGFloat endStackPosition = endLimit_ - kMinStackStaggerAmount -
700 [self staggerOffsetForIndexFromEdge:indexFromEnd];
701 return std::min(fannedOutPosition, endStackPosition);
702 }
703
704 - (void)moveCardAtIndex:(NSUInteger)index byAmount:(CGFloat)amount {
705 DCHECK(index < [cards_ count]);
706 [self moveCard:cards_[index] byAmount:amount];
707 }
708
709 - (void)moveCard:(StackCard*)card byAmount:(CGFloat)amount {
710 DCHECK(card);
711 LayoutRect layout = card.layout;
712 if (layoutIsVertical_) {
713 layout.position.leading = layoutAxisPosition_ - 0.5 * card.size.width;
714 layout.position.originY += amount;
715 } else {
716 layout.position.leading += amount;
717 layout.position.originY = layoutAxisPosition_ - 0.5 * card.size.height;
718 }
719 card.layout = layout;
720 }
721
722 - (void)moveCardsFromIndex:(NSUInteger)startIndex
723 toIndex:(NSUInteger)endIndex
724 byAmount:(CGFloat)amount {
725 DCHECK(startIndex <= endIndex);
726 DCHECK(endIndex < [cards_ count]);
727 for (NSUInteger i = startIndex; i <= endIndex; ++i) {
728 [self moveCardAtIndex:i byAmount:amount];
729 }
730 }
731
732 - (void)moveOriginOfCardAtIndex:(NSUInteger)index toOffset:(CGFloat)offset {
733 DCHECK(index < [cards_ count]);
734 StackCard* card = [cards_ objectAtIndex:index];
735 CGFloat startingOffset = [self cardOffsetOnLayoutAxis:card];
736 [self moveCard:card byAmount:offset - startingOffset];
737 }
738
739 // Constrains offset to satisfy the following constraints:
740 // - >= |kMinStackStaggerAmount| away from origin of constraining neighbor.
741 // - <= |maximumCardSeparation:| away from origin of constraining neighbor.
742 // - <= |maximumOffsetForCardAtIndex:index|.
743 - (CGFloat)constrainedOffset:(CGFloat)offset
744 forCardAtIndex:(NSInteger)index
745 constrainingNeighborIsPrevious:(BOOL)isPrevious {
746 DCHECK(index < (NSInteger)[cards_ count]);
747 if (isPrevious)
748 DCHECK(index > 0);
749 else
750 DCHECK(index < (NSInteger)[cards_ count] - 1);
751
752 CGFloat constrainingIndex = isPrevious ? index - 1 : index + 1;
753 StackCard* constrainingCard = [cards_ objectAtIndex:constrainingIndex];
754 CGFloat constrainingCardOffset =
755 [self cardOffsetOnLayoutAxis:constrainingCard];
756 // Ensures that the above constraints are mutually satisfiable.
757 DCHECK(constrainingCardOffset <=
758 [self maximumOffsetForCardAtIndex:constrainingIndex]);
759
760 CGFloat minOffset, maxOffset;
761 if (isPrevious) {
762 minOffset = constrainingCardOffset + kMinStackStaggerAmount;
763 maxOffset = constrainingCardOffset + [self maximumCardSeparation];
764 maxOffset = std::min(maxOffset, [self maximumOffsetForCardAtIndex:index]);
765 } else {
766 minOffset = constrainingCardOffset - [self maximumCardSeparation];
767 maxOffset = constrainingCardOffset - kMinStackStaggerAmount;
768 maxOffset = std::min(maxOffset, [self maximumOffsetForCardAtIndex:index]);
769 }
770 DCHECK(minOffset <= maxOffset);
771 offset = std::max(offset, minOffset);
772 offset = std::min(offset, maxOffset);
773 return offset;
774 }
775
776 // If |towardsEnd|, then all cards up to and including the last card are moved,
777 // with each card being constrained by the position of its previous neighbor.
778 // Otherwise, all cards down to but *not* including the first card are moved,
779 // with each card being constrained by the position of its following neighbor.
780 // NOTE: It is assumed that at the time of calling this method that the
781 // boundary card for the movement (i.e., the card before |index| if
782 // |towardsEnd|, the card after |index| otherwise), if it exists, is in its
783 // desired position, as constraining is performed in this method with respect
784 // to the position of that boundary card.
785 - (void)moveCardsStartingAtIndex:(NSInteger)index
786 towardsEnd:(BOOL)towardsEnd
787 withDrivingDelta:(CGFloat)drivingDelta {
788 const CGFloat kDecayFactor = 2.0;
789 DCHECK(index < (NSInteger)[cards_ count]);
790 DCHECK(index >= 0);
791
792 NSInteger numCardsToMove;
793 if (towardsEnd)
794 numCardsToMove = [cards_ count] - index;
795 else
796 numCardsToMove = index;
797
798 NSInteger currentIndex = index;
799 CGFloat currentDelta = drivingDelta / kDecayFactor;
800 for (int i = 0; i < numCardsToMove; i++) {
801 StackCard* card = [cards_ objectAtIndex:currentIndex];
802 CGFloat cardStartingOffset = [self cardOffsetOnLayoutAxis:card];
803 CGFloat cardEndingOffset =
804 [self constrainedOffset:cardStartingOffset + currentDelta
805 forCardAtIndex:currentIndex
806 constrainingNeighborIsPrevious:towardsEnd];
807 [self moveOriginOfCardAtIndex:currentIndex toOffset:cardEndingOffset];
808
809 currentIndex = towardsEnd ? currentIndex + 1 : currentIndex - 1;
810 currentDelta = (cardEndingOffset - cardStartingOffset) / kDecayFactor;
811 }
812 }
813
814 // Moves cards as follows:
815 // - the card at |firstIndex| moves by |firstDelta|.
816 // - the card at |secondIndex| moves by |secondDelta|.
817 // - the cards in-between move by a combination of |firstDelta| and
818 // |secondDelta|, with the contribution of each being weighted by the
819 // closeness of the card's starting position to the starting positions of the
820 // cards at |firstIndex| and |secondIndex| respectively.
821 // Each card is constrained to be within its maximum offset, and each card
822 // other than the first is constrained by the position of its previous
823 // neighbor.
824 // NOTE: It is assumed that at the time of calling this method the card before
825 // |firstIndex| and the card after |secondIndex|, if they exist, are not
826 // necessarily in their desired positions. Hence, no constraining is performed
827 // in this method with respect to the positions of those boundary cards.
828 - (void)blendOffsetsOfCardsBetweenFirstIndex:(NSInteger)firstIndex
829 secondIndex:(NSInteger)secondIndex
830 withFirstDelta:(CGFloat)firstDelta
831 secondDelta:(CGFloat)secondDelta {
832 DCHECK(firstIndex < secondIndex);
833 DCHECK(secondIndex < (NSInteger)[cards_ count]);
834 StackCard* firstCard = [cards_ objectAtIndex:firstIndex];
835 CGFloat firstStartingOffset = [self cardOffsetOnLayoutAxis:firstCard];
836 StackCard* secondCard = [cards_ objectAtIndex:secondIndex];
837 CGFloat secondStartingOffset = [self cardOffsetOnLayoutAxis:secondCard];
838 CGFloat firstEndingOffset = firstStartingOffset + firstDelta;
839 CGFloat secondEndingOffset = secondStartingOffset + secondDelta;
840
841 // Move each card by a combination of |firstDelta| and |secondDelta|, with
842 // the contribution of each being weighted by the card's closeness
843 // to |firstStartingOffset| and |secondStartingOffset| respectively.
844 for (NSInteger i = firstIndex; i <= secondIndex; i++) {
845 StackCard* card = [cards_ objectAtIndex:i];
846 CGFloat cardStartingOffset = [self cardOffsetOnLayoutAxis:card];
847 CGFloat weightOfSecondDelta = (cardStartingOffset - firstStartingOffset) /
848 (secondStartingOffset - firstStartingOffset);
849 CGFloat weightOfFirstDelta = 1 - weightOfSecondDelta;
850 CGFloat cardEndingOffset = weightOfFirstDelta * firstEndingOffset +
851 weightOfSecondDelta * secondEndingOffset;
852 // First card being moved is not constrained to previous neighbor but is
853 // constrained to be within its maximum offset unless it is the first card
854 // of the deck, which is allowed to move off its maximum offset for an
855 // overpinch effect.
856 if (i == firstIndex) {
857 if (i > 0) {
858 cardEndingOffset = std::min(
859 cardEndingOffset, [self maximumOffsetForCardAtIndex:firstIndex]);
860 }
861 } else {
862 cardEndingOffset = [self constrainedOffset:cardEndingOffset
863 forCardAtIndex:i
864 constrainingNeighborIsPrevious:YES];
865 }
866 [self moveOriginOfCardAtIndex:i toOffset:cardEndingOffset];
867 }
868 }
869
870 // - The cards at indices between |firstCardIndex| and |secondCardIndex|
871 // inclusive are blended proportionally between the ending positions of those
872 // two cards.
873 // - The cards at indices < |firstCardIndex| are adjusted based on |firstDelta|
874 // with an exponential decay.
875 // - The cards at indices > |secondCardIndex| are adjusted based on
876 // |secondDelta| with an exponential decay.
877 - (void)handleMultitouchWithFirstDelta:(CGFloat)firstDelta
878 secondDelta:(CGFloat)secondDelta
879 firstCardIndex:(NSInteger)firstCardIndex
880 secondCardIndex:(NSInteger)secondCardIndex
881 decayOnOverpinch:(BOOL)decayOnOverpinch {
882 DCHECK(firstCardIndex < secondCardIndex);
883 NSInteger numCards = (NSInteger)[cards_ count];
884 DCHECK(secondCardIndex < numCards);
885
886 treatOverExtensionAsScroll_ = NO;
887 previousFirstPinchCardIndex_ = firstCardIndex;
888 previousSecondPinchCardIndex_ = secondCardIndex;
889
890 // Temporarily turn off updates to the cards' views as this method might be
891 // being called from within an animation, and updating the coordinates of a
892 // |UIView| multiple times while it is animating can cause undesired
893 // behavior.
894 [self setSynchronizeCardViews:NO];
895
896 if (decayOnOverpinch) {
897 if ([self overextensionTowardStartOnCardAtIndex:firstCardIndex] ||
898 (firstCardIndex == 0 && [self overextensionTowardEndOnFirstCard]))
899 firstDelta /= kOverextensionDecayFactor;
900 if ([self overextensionTowardStartOnCardAtIndex:secondCardIndex] ||
901 (secondCardIndex == 0 && [self overextensionTowardEndOnFirstCard]))
902 secondDelta /= kOverextensionDecayFactor;
903 }
904
905 // Blend the positions of the cards between the two touched cards (inclusive).
906 // This step must be performed first, as the following two calls assume that
907 // |firstCardIndex| and |secondCardIndex| are in their correct positions when
908 // calculating constraints for positions of other cards.
909 [self blendOffsetsOfCardsBetweenFirstIndex:firstCardIndex
910 secondIndex:secondCardIndex
911 withFirstDelta:firstDelta
912 secondDelta:secondDelta];
913
914 // Adjust the cards after |secondCardIndex| and before |firstCardIndex|.
915 if (secondCardIndex < numCards - 1) {
916 [self moveCardsStartingAtIndex:secondCardIndex + 1
917 towardsEnd:YES
918 withDrivingDelta:secondDelta];
919 }
920 if (firstCardIndex > 0) {
921 [self moveCardsStartingAtIndex:firstCardIndex - 1
922 towardsEnd:NO
923 withDrivingDelta:firstDelta];
924 }
925
926 // Perform start and end capping, allowing overextension on the start stack as
927 // determined by the offset of the first pinched card.
928 CGFloat startLimit = [self
929 startStackLimitAllowingForOverextensionOnCardAtIndex:firstCardIndex];
930 [self layOutEdgeStacksWithStartLimit:startLimit];
931 [self setSynchronizeCardViews:YES];
932 }
933
934 - (CGFloat)startStackLimitAllowingForOverextensionOnCardAtIndex:
935 (NSUInteger)index {
936 DCHECK(index < [cards_ count]);
937 if (![self overextensionTowardStartOnCardAtIndex:index])
938 return startLimit_;
939 // Calculate the start limit that will lay the start stack into place around
940 // the card at |index|.
941 CGFloat startLimit =
942 [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:index]] -
943 [self staggerOffsetForIndexFromEdge:index];
944 return std::max(startLimit, [self limitOfOverextensionTowardStart]);
945 }
946
947 - (void)layOutEdgeStacksWithStartLimit:(CGFloat)startLimit {
948 [self layOutStartStackWithLimit:startLimit];
949 [self layOutEndStack];
950 }
951
952 - (void)layOutStartStack {
953 [self layOutStartStackWithLimit:startLimit_];
954 }
955
956 - (void)layOutStartStackWithLimit:(CGFloat)limit {
957 lastStartStackCardIndex_ =
958 [self computeEdgeStackBoundaryIndex:YES withVisualStackLimit:limit];
959 if (lastStartStackCardIndex_ == -1)
960 return;
961
962 // Position the cards. Cards up to the last card of the start stack are
963 // staggered backwards from the start stack's inner edge.
964 CGFloat stackInnerEdge =
965 [self computeEdgeStackInnerEdge:YES withVisualStackLimit:limit];
966 for (NSInteger i = 0; i <= lastStartStackCardIndex_; i++) {
967 CGFloat distanceFromInnerEdge =
968 (lastStartStackCardIndex_ - i) * kMinStackStaggerAmount;
969 CGFloat offset = std::max(limit, stackInnerEdge - distanceFromInnerEdge);
970 [self moveOriginOfCardAtIndex:i toOffset:offset];
971 }
972 }
973
974 - (void)layOutEndStack {
975 NSInteger numCards = [cards_ count];
976 // When laying out the stack, leave enough room so that the last card is
977 // visible.
978 CGFloat visualLimit = endLimit_ - kMinStackStaggerAmount;
979 firstEndStackCardIndex_ =
980 [self computeEdgeStackBoundaryIndex:NO withVisualStackLimit:visualLimit];
981 if (firstEndStackCardIndex_ == numCards)
982 return;
983
984 // Position the cards. Cards from the first card of the end stack are
985 // staggered forwards from the end stack's inner edge.
986 CGFloat stackInnerEdge =
987 [self computeEdgeStackInnerEdge:NO withVisualStackLimit:visualLimit];
988 for (NSInteger i = firstEndStackCardIndex_; i < numCards; i++) {
989 CGFloat distanceFromInnerEdge =
990 (i - firstEndStackCardIndex_) * kMinStackStaggerAmount;
991 CGFloat offset =
992 std::min(visualLimit, stackInnerEdge + distanceFromInnerEdge);
993 [self moveOriginOfCardAtIndex:i toOffset:offset];
994 }
995 }
996
997 - (NSInteger)computeEdgeStackBoundaryIndex:(BOOL)startStack
998 withVisualStackLimit:(CGFloat)visualStackLimit {
999 NSInteger numCards = [cards_ count];
1000 NSInteger boundaryIndex = startStack ? -1 : numCards;
1001 for (NSInteger i = 0; i < numCards; ++i) {
1002 StackCard* card = [cards_ objectAtIndex:i];
1003 CGFloat uncappedPosition = [self cardOffsetOnLayoutAxis:card];
1004 if (startStack) {
1005 CGFloat pushThreshold =
1006 visualStackLimit + [self pushThresholdForIndexFromEdge:i];
1007 if (uncappedPosition <= pushThreshold)
1008 boundaryIndex = i;
1009 } else {
1010 NSInteger indexFromEnd = numCards - 1 - i;
1011 CGFloat pushThreshold =
1012 visualStackLimit - [self pushThresholdForIndexFromEdge:indexFromEnd];
1013 if (uncappedPosition >= pushThreshold) {
1014 boundaryIndex = i;
1015 break;
1016 }
1017 }
1018 }
1019 return boundaryIndex;
1020 }
1021
1022 - (CGFloat)computeEdgeStackInnerEdge:(BOOL)startStack
1023 withVisualStackLimit:(CGFloat)visualStackLimit {
1024 NSInteger boundaryIndex =
1025 startStack ? lastStartStackCardIndex_ : firstEndStackCardIndex_;
1026 DCHECK(boundaryIndex >= 0);
1027 DCHECK(boundaryIndex < (NSInteger)[cards_ count]);
1028 StackCard* card = [cards_ objectAtIndex:boundaryIndex];
1029 CGFloat offset = [self cardOffsetOnLayoutAxis:card];
1030 NSUInteger indexFromEnd = [cards_ count] - 1 - boundaryIndex;
1031 CGFloat cap = startStack
1032 ? visualStackLimit +
1033 [self staggerOffsetForIndexFromEdge:boundaryIndex]
1034 : visualStackLimit -
1035 [self staggerOffsetForIndexFromEdge:indexFromEnd];
1036 return startStack ? std::max(cap, offset) : std::min(cap, offset);
1037 }
1038
1039 - (CGFloat)fannedStackLength {
1040 if ([cards_ count] == 0)
1041 return 0;
1042 CGFloat cardLength = [self layoutLength:cardSize_];
1043 return maxStagger_ * ([cards_ count] - 1) + cardLength;
1044 }
1045
1046 - (CGFloat)maximumStackLength {
1047 if ([cards_ count] == 0)
1048 return 0;
1049 CGFloat cardLength = [self layoutLength:cardSize_];
1050 return [self maximumCardSeparation] * ([cards_ count] - 1) + cardLength;
1051 }
1052
1053 - (CGFloat)fullyCollapsedStackLength {
1054 CGFloat staggerLength =
1055 kMinStackStaggerAmount * (kMaxVisibleStaggerCount - 1);
1056 return [self layoutLength:cardSize_] + staggerLength;
1057 }
1058
1059 - (CGFloat)layoutLength:(CGSize)size {
1060 return layoutIsVertical_ ? size.height : size.width;
1061 }
1062
1063 - (CGFloat)layoutOffset:(LayoutRectPosition)position {
1064 return layoutIsVertical_ ? position.originY : position.leading;
1065 }
1066
1067 - (CGFloat)cardOffsetOnLayoutAxis:(StackCard*)card {
1068 return [self layoutOffset:card.layout.position];
1069 }
1070
1071 - (CGFloat)staggerOffsetForIndexFromEdge:(NSInteger)countFromEdge {
1072 return std::min(countFromEdge, kMaxVisibleStaggerCount - 1) *
1073 kMinStackStaggerAmount;
1074 }
1075
1076 - (CGFloat)pushThresholdForIndexFromEdge:(NSInteger)countFromEdge {
1077 return std::min(countFromEdge, kMaxVisibleStaggerCount) *
1078 kMinStackStaggerAmount;
1079 }
1080
1081 - (BOOL)cardIsCovered:(StackCard*)card {
1082 NSUInteger index = [cards_ indexOfObject:card];
1083 DCHECK(index != NSNotFound);
1084 DCHECK(index < [cards_ count]);
1085
1086 if (index == [cards_ count] - 1)
1087 return NO;
1088
1089 // Card positions are non-decreasing, and cards are all the same size, so a
1090 // card is completely covered iff the next card is in exactly the same
1091 // position (in terms of screen coordinates).
1092 StackCard* nextCard = [cards_ objectAtIndex:(index + 1)];
1093 LayoutRectPosition position =
1094 AlignLayoutRectPositionToPixel(card.layout.position);
1095 LayoutRectPosition nextPosition =
1096 AlignLayoutRectPositionToPixel(nextCard.layout.position);
1097 return LayoutRectPositionEqualToPosition(position, nextPosition);
1098 }
1099
1100 - (BOOL)cardIsCollapsed:(StackCard*)card {
1101 NSUInteger index = [cards_ indexOfObject:card];
1102 DCHECK(index != NSNotFound);
1103 DCHECK(index < [cards_ count]);
1104
1105 // Last card is collapsed if close enough to edge that title isn't visible.
1106 if (index == [cards_ count] - 1) {
1107 CGFloat cardOffset = [self cardOffsetOnLayoutAxis:card];
1108 CGFloat edgeOffset = endLimit_ - kMinStackStaggerAmount;
1109 return cardOffset >= edgeOffset;
1110 }
1111 CGFloat separation =
1112 [self distanceBetweenCardAtIndex:index andCardAtIndex:(index + 1)];
1113 return separation <= kMinStackStaggerAmount;
1114 }
1115
1116 - (BOOL)cardLabelCovered:(StackCard*)card {
1117 NSUInteger index = [cards_ indexOfObject:card];
1118 CGFloat labelOffset = [card.view titleLabel].frame.size.height;
1119 if (index == [cards_ count] - 1) {
1120 CGFloat cardOffset = [self cardOffsetOnLayoutAxis:card];
1121 CGFloat edgeOffset = endLimit_ - labelOffset;
1122 return cardOffset >= edgeOffset;
1123 } else {
1124 CGFloat separation =
1125 [self distanceBetweenCardAtIndex:index andCardAtIndex:(index + 1)];
1126 return separation <= labelOffset;
1127 }
1128 }
1129
1130 - (void)setSynchronizeCardViews:(BOOL)synchronizeViews {
1131 for (StackCard* card in cards_.get()) {
1132 card.synchronizeView = synchronizeViews;
1133 }
1134 }
1135
1136 - (BOOL)isInStartStack:(NSUInteger)index {
1137 DCHECK(index < [cards_ count]);
1138 return ((NSInteger)index <= lastStartStackCardIndex_);
1139 }
1140
1141 - (BOOL)isInEndStack:(NSUInteger)index {
1142 DCHECK(index < [cards_ count]);
1143 return ((NSInteger)index >= firstEndStackCardIndex_);
1144 }
1145
1146 - (BOOL)isInEdgeStack:(NSUInteger)index {
1147 return ([self isInStartStack:index] || [self isInEndStack:index]);
1148 }
1149
1150 - (BOOL)stackIsFullyCollapsed {
1151 NSInteger numCards = [cards_ count];
1152 if (numCards == 0)
1153 return YES;
1154 return (lastStartStackCardIndex_ == (numCards - 1));
1155 }
1156
1157 - (BOOL)stackIsFullyFannedOut {
1158 for (NSUInteger i = 0; i < [cards_ count]; i++) {
1159 CGFloat offset = [self cardOffsetOnLayoutAxis:[cards_ objectAtIndex:i]];
1160 if (offset < [self cappedFanoutOffsetForCardAtIndex:i])
1161 return NO;
1162 }
1163 return YES;
1164 }
1165
1166 - (BOOL)stackIsFullyOverextended {
1167 NSInteger numCards = [cards_ count];
1168 if (numCards == 0)
1169 return YES;
1170
1171 // Test for being fully overextended toward the start.
1172 StackCard* lastCard = [cards_ objectAtIndex:numCards - 1];
1173 CGFloat lastCardOrigin = [self cardOffsetOnLayoutAxis:lastCard];
1174 // Note that -limitOfOverextensionTowardStart is defined with respect to the
1175 // *start* of the stack.
1176 if ((lastCardOrigin - [self staggerOffsetForIndexFromEdge:numCards - 1]) <=
1177 [self limitOfOverextensionTowardStart])
1178 return YES;
1179
1180 // Test for being fully overextended toward the end.
1181 StackCard* firstCard = [cards_ firstObject];
1182 return ([self cardOffsetOnLayoutAxis:firstCard] >=
1183 [self limitOfOverscrollTowardEnd]);
1184 }
1185
1186 - (CGFloat)overextensionAmount {
1187 if ([cards_ count] == 0)
1188 return 0;
1189 return std::abs([self cardOffsetOnLayoutAxis:[cards_ firstObject]] -
1190 startLimit_);
1191 }
1192
1193 - (NSUInteger)fannedStackCount {
1194 return floor((endLimit_ - startLimit_) / maxStagger_);
1195 }
1196
1197 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698