iOS icon jiggle algorithm

前端 未结 11 846
执笔经年
执笔经年 2020-12-12 16:31

I am writing an iPad app that presents user documents similar to the way Pages presents them (as large icons of the actual document). I also want to mimic the jiggling beha

相关标签:
11条回答
  • 2020-12-12 17:10


    @Vic320's answer is good but personally I don't like the translation. I've edited his code to provide a solution that I personally feel looks more like the springboard wobble effect. Mostly, it's achieved by adding a little randomness and focusing on rotation, without translation:

    #define degreesToRadians(x) (M_PI * (x) / 180.0)
    #define kAnimationRotateDeg 1.0
    
    - (void)startJiggling {
        NSInteger randomInt = arc4random_uniform(500);
        float r = (randomInt/500.0)+0.5;
    
        CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
        CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));
    
         self.transform = leftWobble;  // starting point
    
         [[self layer] setAnchorPoint:CGPointMake(0.5, 0.5)];
    
         [UIView animateWithDuration:0.1
                               delay:0
                             options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                           animations:^{ 
                                     [UIView setAnimationRepeatCount:NSNotFound];
                                     self.transform = rightWobble; }
                          completion:nil];
    }
    - (void)stopJiggling {
        [self.layer removeAllAnimations];
        self.transform = CGAffineTransformIdentity;
    }
    


    Credit where credit's due though, @Vic320's answer provided the basis for this code so +1 for that.

    0 讨论(0)
  • 2020-12-12 17:11

    OK, so the openspringboard code didn't quite do it for me but I did allow me to create some code that I think is a bit better, still not prefect but better. If anyone has suggestions to make this better, I would love to hear them... (add this to the subclass of the view(s) you want to jiggle)

    #define degreesToRadians(x) (M_PI * (x) / 180.0)
    #define kAnimationRotateDeg 1.0
    #define kAnimationTranslateX 2.0
    #define kAnimationTranslateY 2.0
    
    - (void)startJiggling:(NSInteger)count {
    
        CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? +1 : -1 ) ));
        CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? -1 : +1 ) ));
        CGAffineTransform moveTransform = CGAffineTransformTranslate(rightWobble, -kAnimationTranslateX, -kAnimationTranslateY);
        CGAffineTransform conCatTransform = CGAffineTransformConcat(rightWobble, moveTransform);
    
        self.transform = leftWobble;  // starting point
    
        [UIView animateWithDuration:0.1
                              delay:(count * 0.08)
                            options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                         animations:^{ self.transform = conCatTransform; }
                         completion:nil];
    }
    
    - (void)stopJiggling {
        [self.layer removeAllAnimations];
        self.transform = CGAffineTransformIdentity;  // Set it straight 
    }
    
    0 讨论(0)
  • 2020-12-12 17:11

    For the benefit of others who come along in the future, I felt the jiggle offered by @Vic320 was a little too robotic and comparing it to Keynote it was a little too strong and not organic (random?) enough. So in the spirit of sharing, here is the code I built into my subclass of UIView... my view controller keeps an array of these objects and when the user taps the Edit button, the view controller sends the startJiggling message to each, followed by a stopJiggling message when the user presses the Done button.

    - (void)startJiggling
    {
        // jiggling code based off the folks on stackoverflow.com:
        // http://stackoverflow.com/questions/6604356/ios-icon-jiggle-algorithm
    
    #define degreesToRadians(x) (M_PI * (x) / 180.0)
    #define kAnimationRotateDeg 0.1
        jiggling = YES;
        [self wobbleLeft];
    }
    
    - (void)wobbleLeft
    {
        if (jiggling) {
            NSInteger randomInt = arc4random()%500;
            float r = (randomInt/500.0)+0.5;
    
            CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
            CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));
    
            self.transform = leftWobble;  // starting point
    
            [UIView animateWithDuration:0.1
                          delay:0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{ self.transform = rightWobble; }
                     completion:^(BOOL finished) { [self wobbleRight]; }
              ];
        }
    }
    
    - (void)wobbleRight
    {
        if (jiggling) {
    
            NSInteger randomInt = arc4random()%500;
            float r = (randomInt/500.0)+0.5;
    
            CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( (kAnimationRotateDeg * -1.0) - r ));
            CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg + r ));
    
            self.transform = rightWobble;  // starting point
    
            [UIView animateWithDuration:0.1
                          delay:0
                        options:UIViewAnimationOptionAllowUserInteraction
                     animations:^{ self.transform = leftWobble; }
                     completion:^(BOOL finished) { [self wobbleLeft]; }
             ];
        }
    
    }
    - (void)stopJiggling
    {
        jiggling = NO;
        [self.layer removeAllAnimations];
        [self setTransform:CGAffineTransformIdentity];
        [self.layer setAnchorPoint:CGPointMake(0.5, 0.5)];
    }
    
    0 讨论(0)
  • 2020-12-12 17:18

    I reverse engineered Apple Stringboard, and modified little bit their animation, and code below is really good stuff.

    + (CAAnimationGroup *)jiggleAnimation {
    CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
    position.keyPath = @"position";
    position.values = @[
                        [NSValue valueWithCGPoint:CGPointZero],
                        [NSValue valueWithCGPoint:CGPointMake(-1, 0)],
                        [NSValue valueWithCGPoint:CGPointMake(1, 0)],
                        [NSValue valueWithCGPoint:CGPointMake(-1, 1)],
                        [NSValue valueWithCGPoint:CGPointMake(1, -1)],
                        [NSValue valueWithCGPoint:CGPointZero]
                        ];
    position.timingFunctions = @[
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                                 ];
    position.additive = YES;
    
    CAKeyframeAnimation *rotation = [CAKeyframeAnimation animation];
    rotation.keyPath = @"transform.rotation";
    rotation.values = @[
                        @0,
                        @0.03,
                        @0,
                        [NSNumber numberWithFloat:-0.02]
                        ];
    rotation.timingFunctions = @[
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                 [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]
                                 ];
    
    CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
    group.animations = @[ position, rotation ];
    group.duration = 0.3;
    group.repeatCount = HUGE_VALF;
    group.beginTime = arc4random() % 30 / 100.f;
    return group;
    }
    

    Original Apple jiggle animation:

    CAKeyframeAnimation *position = [CAKeyframeAnimation animation];
    position.beginTime = 0.8;
    position.duration = 0.25;
    position.values = @[[NSValue valueWithCGPoint:CGPointMake(-1, -1)],
                        [NSValue valueWithCGPoint:CGPointMake(0, 0)],
                        [NSValue valueWithCGPoint:CGPointMake(-1, 0)],
                        [NSValue valueWithCGPoint:CGPointMake(0, -1)],
                        [NSValue valueWithCGPoint:CGPointMake(-1, -1)]];
    position.calculationMode = @"linear";
    position.removedOnCompletion = NO;
    position.repeatCount = CGFLOAT_MAX;
    position.beginTime = arc4random() % 25 / 100.f;
    position.additive = YES;
    position.keyPath = @"position";
    
    CAKeyframeAnimation *transform = [CAKeyframeAnimation animation];
    transform.beginTime = 2.6;
    transform.duration = 0.25;
    transform.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
    transform.values = @[@(-0.03525565),@(0.03525565),@(-0.03525565)];
    transform.calculationMode = @"linear";
    transform.removedOnCompletion = NO;
    transform.repeatCount = CGFLOAT_MAX;
    transform.additive = YES;
    transform.beginTime = arc4random() % 25 / 100.f;
    transform.keyPath = @"transform";
    
    [self.dupa.layer addAnimation:position forKey:nil];
    [self.dupa.layer addAnimation:transform forKey:nil];
    
    0 讨论(0)
  • 2020-12-12 17:19

    Paul Popiel gave an excellent answer to this above and I am forever indebted to him for it. There is one small problem I found with his code and that's that it doesn't work well if that routine is called multiple times - the layer animations appear to sometimes get lost or deactivated.

    Why call it more than once? I'm implementing it via a UICollectionView, and as the cells are dequeued or moved, I need to reestablish the wiggle. With Paul's original code, my cells would often stop wiggling if they scrolled off screen despite my trying to reestablish the wiggle within the dequeue and the willDisplay callback. However, by giving the two animations named keys, it always works reliably even if called twice on a cell.

    This is almost all Paul's code with the above small fix, plus I've created it as an extension to UIView and added a Swift 4 compatible stopWiggle.

    private func degreesToRadians(_ x: CGFloat) -> CGFloat {
        return .pi * x / 180.0
    }
    
    extension UIView {
        func startWiggle() {
            let duration: Double = 0.25
            let displacement: CGFloat = 1.0
            let degreesRotation: CGFloat = 2.0
            let negativeDisplacement = -1.0 * displacement
            let position = CAKeyframeAnimation.init(keyPath: "position")
            position.beginTime = 0.8
            position.duration = duration
            position.values = [
                NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
                NSValue(cgPoint: CGPoint(x: 0, y: 0)),
                NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: 0)),
                NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
                NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
            ]
            position.calculationMode = "linear"
            position.isRemovedOnCompletion = false
            position.repeatCount = Float.greatestFiniteMagnitude
            position.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
            position.isAdditive = true
    
            let transform = CAKeyframeAnimation.init(keyPath: "transform")
            transform.beginTime = 2.6
            transform.duration = duration
            transform.valueFunction = CAValueFunction(name: kCAValueFunctionRotateZ)
            transform.values = [
                degreesToRadians(-1.0 * degreesRotation),
                degreesToRadians(degreesRotation),
                degreesToRadians(-1.0 * degreesRotation)
            ]
            transform.calculationMode = "linear"
            transform.isRemovedOnCompletion = false
            transform.repeatCount = Float.greatestFiniteMagnitude
            transform.isAdditive = true
            transform.beginTime = CFTimeInterval(Float(arc4random()).truncatingRemainder(dividingBy: Float(25)) / Float(100))
    
            self.layer.add(position, forKey: "bounce")
            self.layer.add(transform, forKey: "wiggle")
        }
    
        func stopWiggle() {
            self.layer.removeAllAnimations()
            self.transform = .identity
        }
    }
    

    In case it saves anyone else time implementing this in a UICollectionView, you'll need a few other places to make sure the wiggle stays during moves and scrolls. First, a routine that begins wiggling all the cells that's called at the outset:

    func wiggleAllVisibleCells() {
        if let visible = collectionView?.indexPathsForVisibleItems {
            for ip in visible {
                if let cell = collectionView!.cellForItem(at: ip) {
                    cell.startWiggle()
                }
            }
        }
    }
    

    And as new cells are displayed (from a move or scroll), I reestablish the wiggle:

    override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        // Make sure cells are all still wiggling
        if isReordering {
            cell.startWiggle()
        }
    }
    
    0 讨论(0)
提交回复
热议问题