OLD | NEW |
(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 |
OLD | NEW |