Remove Oscillation from UIAttachmentBehavior in UICollectionView

516 views Asked by At

I am attempting to recreate the spring behavior that you see in the iOS Messages app in my UICollectionView. Like Messages it will have various cell sizes based on the text size. I have created a custom UICollectionViewFlowLayout which does add the behavior to the UICollectionView however the message bubbles continue to oscillate slightly after the user has stopped scrolling. I have tried any number of combinations in the length, damping and spring values but the oscillation never goes away.

After some reading of other stack questions I did find this comment

In order to prevent oscillation it's necessary to dynamically increase the damping factor on a quadratic scale as the attached views get closer and closer to their attachment points. <

But I am not really sure where to get started with implementing something like that on what I currently have. Any help or guidance would be appreciated.

Below is my code on the UICollectionViewFlowLayout that is creating the current effect.

- (void) prepareLayout {
    [super prepareLayout];
    
    CGRect originalRect = (CGRect){.origin = self.collectionView.bounds.origin, .size = self.collectionView.frame.size};
    CGRect visibleRect = CGRectInset(originalRect, -50, -50);
    
    NSArray *itemsInVisibleRectArray = [super layoutAttributesForElementsInRect:visibleRect];
    NSSet *itemsIndexPathsInVisibleRectSet = [NSSet setWithArray:[itemsInVisibleRectArray valueForKey:@"indexPath"]];
    
    
    NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(UIAttachmentBehavior *behaviour, NSDictionary *bindings) {
        BOOL currentlyVisible = [itemsIndexPathsInVisibleRectSet member:[[[behaviour items] firstObject] indexPath]] != nil;
        return !currentlyVisible;
    }];
    NSArray *noLongerVisibleBehaviours = [self.animator.behaviors filteredArrayUsingPredicate:predicate];
    
    [noLongerVisibleBehaviours enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
        [self.animator removeBehavior:obj];
        [self.visibleIndexPathsSet removeObject:[[[obj items] firstObject] indexPath]];
    }];
    
    
    NSPredicate *newPredicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *item, NSDictionary *bindings) {
        BOOL currentlyVisible = [self.visibleIndexPathsSet member:item.indexPath] != nil;
        return !currentlyVisible;
    }];
    NSArray *newlyVisibleItems = [itemsInVisibleRectArray filteredArrayUsingPredicate:newPredicate];
    CGPoint touchLocation = [self.collectionView.panGestureRecognizer locationInView:self.collectionView];
    

    
    [newlyVisibleItems enumerateObjectsUsingBlock:^(UICollectionViewLayoutAttributes *item, NSUInteger idx, BOOL *stop) {
        CGPoint center = item.center;
        UIAttachmentBehavior *springBehaviour = [[UIAttachmentBehavior alloc] initWithItem:item attachedToAnchor:center];
        
        springBehaviour.length = 0.1f;
        springBehaviour.damping = 3.0f;
        springBehaviour.frequency = 2.8f;
        
        if (!CGPointEqualToPoint(CGPointZero, touchLocation)) {
            CGFloat yDistanceFromTouch = fabs(touchLocation.y - springBehaviour.anchorPoint.y);
            CGFloat xDistanceFromTouch = fabs(touchLocation.x - springBehaviour.anchorPoint.x);
            CGFloat scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500.0f;
            
            if (self.latestDelta < 0) {
                center.y += MAX(self.latestDelta, self.latestDelta*scrollResistance);
            }
            else {
                center.y += MIN(self.latestDelta, self.latestDelta*scrollResistance);
            }
            item.center = center;
        }
        
        [self.animator addBehavior:springBehaviour];
        [self.visibleIndexPathsSet addObject:item.indexPath];
    }];
}
1

There are 1 answers

0
scorpiozj On

You can fix it with 2 steps.

  1. add action for the behavior when initials to make sure the center of the cell doesn't change during the animation
springBehaviour.action = ^{
            CGPoint itemCenter = item.center;
            itemCenter.x = center.x;
            item.center = itemCenter;
        };
  1. remove/re-add the behaviors when collectionview stops scrolling. To do this, you need implement a scrollview delegate method and in this method to remove/re-add behaviors.
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    ZZHCollectionFlowLayout *flowLayout = self.collectionView.collectionViewLayout;
    if ([flowLayout isKindOfClass:[ZZHCollectionFlowLayout class]])
    {
        [flowLayout removeAnimationBehavior];
    }
    else
    {
        // Your NSAssertionHandler
    }
}

- (void)removeAnimationBehavior
{
    NSArray *behaviors = self.dynamicAnimator.behaviors;
    [self.dynamicAnimator removeAllBehaviors];
    for (UIDynamicBehavior *obj in behaviors)
    {
        [self.dynamicAnimator addBehavior:obj];
    }
}

BTW, if there is way of changing damping to fix, would like to hear it!