iOS图标晃动算法

22

我正在编写一个iPad应用程序,类似于Pages展示用户文档的方式(以实际文档的大图标形式呈现)。我还想模仿用户点击编辑按钮时产生的晃动行为。这与iPhone和iPad上在主屏幕上长按它们时图标所做的同样摇晃模式相同。

我已经在互联网上搜索了一些算法,但它们只会使视图来回晃动,或者根本不像Apple的晃动。每个图标都会稍微有所不同,似乎有一些随机性在其中。

是否有人拥有或知道可以重新创建相同摇晃模式(或非常接近)的代码?谢谢!!!

11个回答

33


@Vic320的答案很不错,但我个人不太喜欢这种翻译。 我编辑了他的代码,提供了一种我个人认为更像弹簧摆动效果的解决方案。主要是通过添加一些随机性和聚焦于旋转而实现,而没有平移:

#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;
}


虽然要给出应得的赞,但是@Vic320的答案为这段代码提供了基础,所以加1分。


1
还有几点需要注意:不需要使用 [UIView setAnimationRepeatCount:NSNotFound],因为已经设置了 UIViewAnimationOptionRepeat。要停止动画,请执行以下操作:[self.layer removeAllAnimations]; - sdsykes
除了@sdsykes所说的删除所有动画之外,您可能还需要重置变换,以便您的视图不会旋转。self.transform = CGAffineTransformIdentity; - DiscDev
@spotdog13,我在我的回答中提到了"@Vic320的答案为这段代码提供了基础"。我的代码旨在替换他的-startJiggling:方法。他提供了一个-stopJiggling方法,可以删除动画并重置变换,就像你们描述的那样,所以我觉得没有必要在这里重复。 - imnk
@imnk - 我同意,但是如果你跳到最受欢迎的答案(你的答案)并阅读所有评论,你会看到sdsykes关于调用removeAllAnimations而没有提到重置变换的评论。我只是为其他做同样事情的人添加了我的评论。 - DiscDev
对于那些寻找Swift解决方案的人来说,这个很好用,但我卡在了“选项”上。在Swift 2中,似乎“选项”需要一个选项数组。这对我有用:options: [.Autoreverse, .Repeat, .AllowUserInteraction] - Murray Sagal
显示剩余3条评论

11

好的,所以openspringboard代码对我来说并不完全适用,但是它确实让我创建了一些我认为更好的代码,尽管还不够完美。如果有人有建议使其更好,我很乐意听取...(将此添加到您想要抖动的视图的子类中)

#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 
}

9

@mientus 原始的苹果抖动代码使用Swift 4编写,可选参数可调整持续时间(即速度),位移(即位置变化)和角度(即旋转量)。

private func degreesToRadians(_ x: CGFloat) -> CGFloat {
    return .pi * x / 180.0
}

func startWiggle(
    duration: Double = 0.25,
    displacement: CGFloat = 1.0,
    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: nil)
    self.layer.add(transform, forKey: nil)
}

7

Paul Popiel 在上面给出了一个非常好的答案,我永远感激他。但是,我发现他的代码有一个小问题,如果多次调用该函数,则图层动画可能会丢失或停止。

为什么要调用它多次?我通过 UICollectionView 实现它,当单元格被取消排队或移动时,需要重新建立 "wiggle"。使用 Paul 的原始代码,即使在取消排队和 willDisplay 回调中尝试重新建立 "wiggle",我的单元格经常会在滚动超出屏幕后停止晃动。然而,通过为这两个动画命名关键字,即使在单元格上两次调用,它也总是可靠的。

这几乎是所有 Paul 的代码与以上小修复,此外我已将其创建为 UIView 的扩展并添加了 Swift 4 兼容的 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
    }
}

如果这篇文章能帮助到其他人在UICollectionView中实现这个功能,你需要在其他一些地方确保移动和滚动时摆动效果不会消失。首先,在一开始调用一个将所有单元格开始摆动的程序:

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

当新单元格被显示出来(通过移动或滚动),我重新建立了抖动效果:

override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    // Make sure cells are all still wiggling
    if isReordering {
        cell.startWiggle()
    }
}

5

为了完整起见,这是我如何使用显式动画为我的CALayer子类添加动画效果的——灵感来自其他答案。

-(void)stopJiggle 
{
    [self removeAnimationForKey:@"jiggle"];
}

-(void)startJiggle 
{
    const float amplitude = 1.0f; // degrees
    float r = ( rand() / (float)RAND_MAX ) - 0.5f;
    float angleInDegrees = amplitude * (1.0f + r * 0.1f);
    float animationRotate = angleInDegrees / 180. * M_PI; // Convert to radians

    NSTimeInterval duration = 0.1;
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
    animation.duration = duration;
    animation.additive = YES;
    animation.autoreverses = YES;
    animation.repeatCount = FLT_MAX;
    animation.fromValue = @(-animationRotate);
    animation.toValue = @(animationRotate);
    animation.timeOffset = ( rand() / (float)RAND_MAX ) * duration;
    [self addAnimation:animation forKey:@"jiggle"];
}

4
我逆向工程了苹果的Stringboard,并对它们的动画进行了一些修改。下面的代码是真正优秀的东西。
+ (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;
}

原始的苹果晃动动画:

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];

1
哦,我的天啊,我爱你,这太完美了。 - Jota

3
我肯定会因为写乱码而被批评(也许有比我更简单的方法,但我是半个初学者),但这是 Vic320 算法的一种更随机的版本,它可以改变旋转和平移的量。它还会随机决定哪个方向先摆动,如果你有多个物体同时晃动,这会给出一个非常随机的外观。如果效率对你来说很重要,请不要使用此代码。这只是我用自己所知道的方法想到的一个解决方案。
任何人想了解的话,需要#import <QuartzCore/QuartzCore.h>并将其添加到您的链接库中。
#define degreesToRadians(x) (M_PI * (x) / 180.0)

- (void)startJiggling:(NSInteger)count {
    double kAnimationRotateDeg = (double)(arc4random()%5 + 5) / 10;
    double kAnimationTranslateX = (arc4random()%4);
    double kAnimationTranslateY = (arc4random()%4);

    CGAffineTransform leftWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? +1 : -1 ) ));
    CGAffineTransform rightWobble = CGAffineTransformMakeRotation(degreesToRadians( kAnimationRotateDeg * (count%2 ? -1 : +1 ) ));
    int leftOrRight = (arc4random()%2);
    if (leftOrRight == 0){
        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];
    } else if (leftOrRight == 1) {
        CGAffineTransform moveTransform = CGAffineTransformTranslate(leftWobble, -kAnimationTranslateX, -kAnimationTranslateY);
        CGAffineTransform conCatTransform = CGAffineTransformConcat(leftWobble, moveTransform);
        self.transform = rightWobble;  // 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 
}

我喜欢你选择随机化开始抖动的方向。我打算用类似的方式更新我的代码! - tobinjim

2

为了让未来的读者受益,我觉得@Vic320提供的晃动效果有点机械化。与Keynote相比,这个效果太强了,不够自然(随机?)。所以在分享精神的基础上,这是我在我的UIView子类中构建的代码...我的视图控制器维护着这些对象的数组,当用户点击编辑按钮时,视图控制器会向每个对象发送startJiggling消息,当用户按下Done按钮时则发送stopJiggling消息。

- (void)startJiggling
{
    // jiggling code based off the folks on stackoverflow.com:
    // https://dev59.com/22w15IYBdhLWcg3wcrZC

#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)];
}

2

请查看openspringboard项目

具体来说,请在OpenSpringBoard.m中设置setIconAnimation:(BOOL)isAnimating。这应该会给你一些实现的思路。


1

如果有人需要相同的Swift代码

class Animation {

    static func wiggle(_ btn: UIButton) {
        btn.startWiggling()
    }
} 

extension UIView {

func startWiggling() {

    let count = 5
    let kAnimationRotateDeg = 1.0

    let leftDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? +5 : -5)).convertToDegrees()
    let leftWobble = CGAffineTransform(rotationAngle: leftDegrees)

    let rightDegrees = (kAnimationRotateDeg * ((count%2 > 0) ? -10 : +10)).convertToDegrees()
    let rightWobble = CGAffineTransform(rotationAngle: rightDegrees)

    let moveTransform = rightWobble.translatedBy(x: -2.0, y: 2.0)
    let concatTransform = rightWobble.concatenating(moveTransform)

    self.transform = leftWobble

    UIView.animate(withDuration: 0.1,
                   delay: 0.1,
                   options: [.allowUserInteraction, .repeat, .autoreverse],
                   animations: {
                    UIView.setAnimationRepeatCount(3)
                    self.transform = concatTransform
    }, completion: { success in
        self.layer.removeAllAnimations()
        self.transform = .identity
    })
}
}

只需调用

    Animation.wiggle(viewToBeAnimated)

最好的做法是编写一个包装器来调用函数,这样即使您必须更改函数参数或函数名称,也不需要在代码中的每个位置都重新编写它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接