Алгоритм перемешивания иконок iOS - PullRequest
20 голосов
/ 07 июля 2011

Я пишу приложение для iPad, которое представляет пользовательские документы аналогично тому, как их представляет Pages (в виде больших значков реального документа).Я также хочу имитировать покачивание, когда пользователь нажимает кнопку редактирования.Это тот же шаблон покачивания, который делают значки на главном экране, когда вы нажимаете и удерживаете их на iPhone и iPad.

Я искал в Интернете и нашел несколько алгоритмов, но они просто заставляют представление качаться взад-вперед, что совсем не похоже на колебания Apple.Кажется, здесь есть некоторая случайность, так как каждая иконка немного по-разному.

Кто-нибудь знает или знает какой-нибудь код, который может воссоздать тот же самый шаблон шатания (или что-то очень похожее на него)?Спасибо !!!

Ответы [ 10 ]

33 голосов
/ 02 сентября 2011


Ответ @ 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 за это.

12 голосов
/ 07 июля 2011

ОК, так что код 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 
}
6 голосов
/ 30 ноября 2012

Для полноты, вот как я анимировал свой подкласс 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 голосов
/ 09 декабря 2017

@ mientus Оригинальный код Apple Jiggle в 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)
}
3 голосов
/ 27 января 2016

Я реверс-инжиниринг Apple 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;
}

Оригинальная анимация Apple Jiggle:

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];
3 голосов
/ 25 мая 2012

В интересах других, кто придет в будущем, я чувствовал, что джигглинг, предложенный @ Vic320, был слишком роботизированным, и, сравнивая его с Keynote, он был слишком сильным и недостаточно органичным (случайным?).Итак, в духе совместного использования, вот код, который я встроил в свой подкласс UIView ... мой контроллер представления сохраняет массив этих объектов, и когда пользователь нажимает кнопку «Редактировать», контроллер представления отправляет сообщение startJiggling каждому, а затемсообщением stopJiggling, когда пользователь нажимает кнопку «Готово».

- (void)startJiggling
{
    // jiggling code based off the folks on stackoverflow.com:
    // /4279713/algoritm-peremeshivaniya-ikonok-ios

#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)];
}
3 голосов
/ 07 июля 2011

Ознакомьтесь с проектом openspringboard .

В частности, setIconAnimation: (BOOL) isAnimating в OpenSpringBoard.m.Это должно дать вам несколько идей о том, как это сделать.

2 голосов
/ 14 декабря 2018

Пол Попил дал превосходный ответ на этот вопрос выше, и я ему за это в долгу. Есть одна небольшая проблема, которую я обнаружил с его кодом, и это то, что он не работает хорошо, если эта процедура вызывается несколько раз - анимации слоев иногда теряются или деактивируются.

Зачем называть это более одного раза? Я реализую это через UICollectionView, и, поскольку ячейки убраны или перемещены, мне нужно восстановить покачивание. С оригинальным кодом Пола мои ячейки часто перестали бы покачиваться, если бы они прокручивались вне экрана, несмотря на мои попытки восстановить покачивание в пределах очереди и обратный вызов willDisplay. Однако, давая две анимации с именем keys, он всегда работает надежно, даже если дважды вызывается в ячейке.

Это почти весь код Пола с небольшим исправлением, описанным выше, плюс я создал его как расширение 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()
    }
}
2 голосов
/ 01 июня 2012

Так что я уверен, что меня будут кричать за написание грязного кода (возможно, есть более простые способы сделать это, о которых я не знаю, потому что я полу-новичок), но это более случайная версия алгоритма 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 
}
0 голосов
/ 07 января 2019

Вот код Swift 4.2 @ mientus '(который сам по себе является обновлением версии Пола Попеля ), как расширение CALayer:

extension CALayer {

    private enum WigglingAnimationKey: String {
        case position = "wiggling_position_animation"
        case transform = "wiggling_transform_animation"
    }

    func startWiggling() {
        let duration = 0.25
        let displacement = 1.0
        let negativeDisplacement = displacement * -1
        let rotationAngle = Measurement(value: 2, unit: UnitAngle.degrees)

        // Position animation
        let positionAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
        positionAnimation.beginTime = 0.8
        positionAnimation.duration = duration
        positionAnimation.values = [
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint.zero),
            NSValue(cgPoint: CGPoint(x: 0, y: negativeDisplacement)),
            NSValue(cgPoint: CGPoint(x: negativeDisplacement, y: negativeDisplacement))
        ]
        positionAnimation.calculationMode = .linear
        positionAnimation.isRemovedOnCompletion = false
        positionAnimation.repeatCount = .greatestFiniteMagnitude
        positionAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)
        positionAnimation.isAdditive = true

        // Rotation animation
        let transformAnimation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.transform))
        transformAnimation.beginTime = 2.6
        transformAnimation.duration = duration
        transformAnimation.valueFunction = CAValueFunction(name: .rotateZ)
        transformAnimation.values = [
            CGFloat(rotationAngle.converted(to: .radians).value * -1),
            CGFloat(rotationAngle.converted(to: .radians).value),
            CGFloat(rotationAngle.converted(to: .radians).value * -1)
        ]
        transformAnimation.calculationMode = .linear
        transformAnimation.isRemovedOnCompletion = false
        transformAnimation.repeatCount = .greatestFiniteMagnitude
        transformAnimation.isAdditive = true
        transformAnimation.beginTime = CFTimeInterval(Float(Int.random(in: 0...25)) / 100)

        self.add(positionAnimation, forKey: WigglingAnimationKey.position.rawValue)
        self.add(transformAnimation, forKey: WigglingAnimationKey.transform.rawValue)
    }

    func stopWiggling() {
        self.removeAnimation(forKey: WigglingAnimationKey.position.rawValue)
        self.removeAnimation(forKey: WigglingAnimationKey.transform.rawValue)
    }
}

Использование (где anyLayer - это CALayer):

// Start animating.
anyLayer.startWiggling()
// Stop animating.
anyLayer.stopWiggling()
...