簡體   English   中英

iOS 圖標抖動算法

[英]iOS icon jiggle algorithm

我正在編寫一個 iPad 應用程序,該應用程序呈現用戶文檔的方式類似於 Pages 呈現它們的方式(作為實際文檔的大圖標)。 我還想模仿用戶點擊編輯按鈕時的抖動行為。 這與您在 iPhone 和 iPad 上點擊並按住主屏幕上的圖標時所產生的抖動模式相同。

我在互聯網上搜索並找到了一些算法,但它們只會導致視圖來回搖擺,這根本不像 Apple 搖晃。 似乎有一些隨機性,因為每個圖標都有點不同。

有沒有人有或知道一些可以重新創建相同的抖動模式(或非常接近它的東西)的代碼? 謝謝!!!


@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。

好的,所以 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 
}

@mientus Swift 4 中的原始Apple Jiggle 代碼,帶有可選參數來調整持續時間(即速度)、位移(即position 變化)和度數(即旋轉量)。

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

為了完整起見,這是我使用顯式 animation 為我的 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"];
}

Paul Popiel在上面給出了一個很好的答案,我永遠感激他。 我在他的代碼中發現了一個小問題,那就是如果多次調用該例程,它就無法正常工作——圖層動畫有時會丟失或停用。

為什么不止一次調用它? 我通過 UICollectionView 實現它,並且隨着單元的出列或移動,我需要重新建立擺動。 使用 Paul 的原始代碼,即使我嘗試在 dequeue 和 willDisplay 回調中重新建立擺動,如果它們滾動到屏幕外,我的單元格通常會停止擺動。 但是,通過將兩個動畫命名為鍵,即使在單元格上調用兩次,它也始終可以可靠地工作。

這幾乎是 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()
    }
}

所以我敢肯定我會因為編寫亂七八糟的代碼而被罵(可能有更簡單的方法可以做到這一點,因為我是半初學者,所以我不知道),但這是 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 
}

我對 Apple Stringboard 進行了逆向工程,並稍微修改了他們的 animation,下面的代碼非常好。

+ (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;
}

原裝蘋果微動 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];

為了將來出現的其他人的利益,我覺得@Vic320 提供的微動有點過於機械化,與 Keynote 相比,它有點太強烈而且不夠有機(隨機?)。 因此,本着分享的精神,這是我在 UIView 的子類中構建的代碼...當用戶按下 Done 按鈕時,通過 stopJiggling 消息。

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

查看openspringboard 項目

特別是,setIconAnimation:(BOOL)isAnimating 在 OpenSpringBoard.m 中。 這應該會給你一些關於如何做到這一點的想法。

如果有人在 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)

最好在您調用的函數上編寫一個包裝器,這樣即使您必須更改 function arguments 或者可能是 ZC1C425268E68385D1AB5074C17A94F14 的名稱,也不會在代碼中到處重寫它。

這是@mientus代碼的Swift 4.2版本(它本身是Paul Popiel 版本的更新),作為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)
    }
}

用法(其中anyLayerCALayer ):

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM