- Physics-Based Behaviors
- Detecting Pauses
- Implementing Snap Zones
- Leveraging Real-World Physics
- Custom Behaviors
- Collection Views and Dynamic Animators
- Building a Dynamic Alert View
- Wrap-up
Collection Views and Dynamic Animators
Leveraging the power of dynamic animators in collection views is possible courtesy of a few UIKit extensions. Dynamic animators add liveliness to your presentations during scrolling and when views enter and leave the system. The dynamic behavior set is identical to that used for normal view animation, but the collection view approach requires a bit more overhead and bookkeeping as views may keep appearing and disappearing during scrolls.
The core of the dynamic animator system is the UIDynamicItem protocol. The UICollectionViewLayoutAttributes class, which represents items in the collection view, conforms to this protocol. Each instance provides the required bounds, center, and transform properties you need to work with dynamic animators. So although you don’t work directly with views, you’re still well set to introduce dynamics.
Custom Flow Layouts
The key to using dynamic animation classes with collection views is to build your own custom UICollectionViewFlowLayout subclass. Flow layouts create organized presentations in your application. Their properties and instance methods specify how the flow sets itself up to place items onscreen. In the most basic form, the layout properties provide you with a geometric vocabulary, where you talk about row spacing, indentation, and item-to-item margins. With custom subclasses, you can extend the class to produce eye-catching and nuanced results.
To support dynamic animation, your custom class must coordinate with an animator instance. You typically set it up in your flow layout initializer by using the UIDynamicAnimator collection view-specific initializer. This prepares the animator for use with your collection view and enables it to take control of reporting item attributes on your behalf. As you’ll see, the dynamic animator takes charge of many methods you normally would have to implement by hand.
The following init method allocates an animator and adds a custom “spinner” behavior. The UIDynamicItemBehavior class enables you to add angular velocity to views, creating a spinning effect, which you see in action in Figure 6-3:
- (instancetype) initWithItemSize: (CGSize) size { if (!(self = [super init])) return self; _animator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self]; _spinner = [[UIDynamicItemBehavior alloc] init]; _spinner.allowsRotation = YES; [_animator addBehavior:_spinner]; self.scrollDirection = UICollectionViewScrollDirectionHorizontal; self.itemSize = size; return self; }
Figure 6-3 Allowing dynamic items to rotate enables you to add angular velocities, causing views to tilt and spin.
Returning Layout Attributes
As mentioned earlier, a dynamic animator can take charge of reporting layout attributes. The following methods do all the work, redirecting the normal geometry through the animator:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { return [_animator itemsInRect:rect]; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath: (NSIndexPath *)indexPath { UICollectionViewLayoutAttributes *dynamicLayoutAttributes = [_animator layoutAttributesForCellAtIndexPath:indexPath]; // Check whether the attributes were properly generated return dynamicLayoutAttributes ? [_animator layoutAttributesForCellAtIndexPath:indexPath] : [super layoutAttributesForItemAtIndexPath:indexPath]; } - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { return YES; }
For safety, the second method checks that the animator properly reports attributes. If it fails, the method falls back to the default implementation.
Updating Behaviors
With collection views, the hardest work involves coordinating items with behaviors. Although you can allow behaviors to control items that are no longer onscreen, as a general rule, you probably want to weed out any items that have left the display and add any items that have moved into place. Listing 6-7 demonstrates this approach.
You start by calculating the onscreen rectangle and request the array of items that appear in that space. Use each item’s index path to compare it to items owned by a behavior. If a behavior item does not appear in the onscreen list, remove it. If an onscreen item isn’t yet owned by the behavior, add it.
Although you mostly just add physics behaviors and let them run, I decided to tie Listing 6-7 to user interaction. The speed and direction of the backing scroll view add “impulses” to each view, nudging their angular velocity in one direction or the other.
Listing 6-7 Adding Physics-Based Animation to Collection Views
// Scroll view delegate method establishes the current speed - (void)scrollViewDidScroll:(UIScrollView *)scrollView { scrollSpeed = scrollView.contentOffset.x - previousScrollViewXOffset; previousScrollViewXOffset = scrollView.contentOffset.x; } // Prepare the flow layout - (void) prepareLayout { [super prepareLayout]; // The collection view isn’t established in init, catch it here. if (!setupDelegate) { setupDelegate = YES; self.collectionView.delegate = self; } // Retrieve onscreen items CGRect currentRect = self.collectionView.bounds; currentRect.size = self.collectionView.frame.size; NSArray *items = [super layoutAttributesForElementsInRect:currentRect]; // Clean up any item that’s now offscreen NSArray *itemPaths = [items valueForKey:@”indexPath”]; for (UICollectionViewLayoutAttributes *item in _spinner.items) { if (![itemPaths containsObject:item.indexPath]) [_spinner removeItem:item]; } // Add all onscreen items NSArray *spinnerPaths = [_spinner.items valueForKey:@”indexPath”]; for (UICollectionViewLayoutAttributes *item in items) { if (![spinnerPaths containsObject:item.indexPath]) [_spinner addItem:item]; } // Add impulses CGFloat impulse = (scrollSpeed / self.collectionView.frame.size.width) * M_PI_4 / 4; for (UICollectionViewLayoutAttributes *item in _spinner.items) { CGAffineTransform t = item.transform; CGFloat rotation = atan2f(t.b, t.a); if (fabs(rotation) > M_PI / 32) impulse = -rotation * 0.01; [_spinner addAngularVelocity:impulse forItem:item]; } }