如何在UILabel中实现数字递增的动画效果

46
我有一个显示数字的标签,想要将其更改为更高的数字,但我想添加一些动态效果。我希望数字能够按照EaseInOut曲线递增到更高的数字,这样它就会加速,然后减速。这个答案展示了如何实现递增(第二个答案,而不是被采纳的答案),但我希望可以通过动画使其递增,并且还可以使其稍微增加大小,然后再次缩小,同时使用EaseInOut曲线。 如何在iPhone SDK中实现运行分数动画 有什么最好的方法来实现这个效果吗? 谢谢
起始/结束数字将由用户输入,我希望它可以在相同的时间内递增至结束数字。因此,如果我有开始10,结束100或开始10,结束1000,我希望它可以在5秒钟内计算到结束数字。
12个回答

77

我实际上为这个问题专门创建了一个名为UICountingLabel的类:

http://github.com/dataxpress/UICountingLabel

它允许你指定计数模式是线性的、缓入、缓出还是缓入/缓出。 缓入/缓出会慢慢地开始计数,加速,然后在您指定的任意时间内缓慢结束。

目前它不支持根据当前值设置标签的实际字体大小,但如果有需要,我可能会添加该功能。 我的大多数布局中的标签都没有太多的空间可以增大或缩小,因此我不确定您要如何使用它。 但是,它的行为完全像一个普通标签,因此您也可以自己更改字体大小。


很高兴你喜欢它 :) 如果你有任何功能请求,请随意在Github上发布! - Tim
这是一个不错的问题,但有没有人知道如何让UICountingLabel在显示小数点时使用逗号而不是句号? - PaperThick
我下载并测试了一个从0到30的计数器,但在3秒内无法正常工作。动画持续时间接近4秒。 - Simon
我已经将这个实现到我的项目中,它完美地工作了。 - Derek Saunders
这个有 Swift 版本吗? - Junaid

20

这是 @malex 在 Swift 3 中的答案。

func incrementLabel(to endValue: Int) {
    let duration: Double = 2.0 //seconds
    DispatchQueue.global().async {
        for i in 0 ..< (endValue + 1) {
            let sleepTime = UInt32(duration/Double(endValue) * 1000000.0)
            usleep(sleepTime)
            DispatchQueue.main.async {
                self.myLabel.text = "\(i)"
            }
        }
    }
}

然而,我强烈建议您只需从GitHub下载这个类并将其拖入您的项目中,因为我的代码中的时间似乎无法正确调整更低/更高的计数数字。这个类很棒,看起来非常好。请参考这篇Medium文章


1
我认为那不正确。我认为sleepTime配置错误了。 - El Tomato
很好的发现。我已经更正了答案,包括持续时间的计算,但是在我的测试中,数学是正确的,但是在睡眠/调度主进程中存在延迟,因此对于上面的代码,完成需要大约4秒而不是输入的2秒。 - Travis M.
1
那个要点代码似乎已经失效了,请更新答案。@TravisM。 - Lal Krishna

18

你可以使用GCD将延迟转移到后台线程。

这是一个值动画的例子(从1到100在10秒钟内)

float animationPeriod = 10;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    for (int i = 1; i < 101; i ++) {
        usleep(animationPeriod/100 * 1000000); // sleep in microseconds
        dispatch_async(dispatch_get_main_queue(), ^{
            yourLabel.text = [NSString stringWithFormat:@"%d", i];
        });
    }
});

当我将这个粘贴到Xcode中时,我在“MAIN_QUEUE”上遇到了一个错误。我不得不用“dispatch_get_main_queue()”替换“MAIN_QUEUE”。否则,这是一个很好的答案。 - user3344977
非常感谢您的发布。上面得到了大量赞同的UICountingLabel答案现在已经无法正常工作,所以这是一个不错的替代方案。 - user3344977
4
为什么在Swift 2.0中我可以直接获取值,而不需要进行“动画”? - Reshad

5
您可以使用标志来确定它是向上还是向下移动。 不使用for循环,而使用while循环。 这样,您将创建一个持续运行的循环,因此您必须找到一种停止它的方法,例如通过按下按钮。

也许我没有解释清楚。我已经在我的问题中添加了更多内容。 我可以使用for循环或while循环从一个数字递增到另一个数字。但是,我希望它能够缓慢加速然后向结束时减速。并且所有这些都在一定的时间内发生。 - Darren
然后你可以使用2个for循环:
  • 一个向上
  • 另一个向下计数
如果您想使用固定的时间量,则必须使用计时器,否则它会以CPU处理能力所能达到的速度运行。
- Maarten Kesselaers
我不需要倒计时。我可以管理循环,我只需要在特定的时间内完成过程。 我能否将计数循环放入计时器中,以便在那段时间内完成过程,就像动画一样?我认为计时器只会告诉它发生的频率。 - Darren
如果您查看您在问题中发布的链接的第一个答案,您会发现计时器将每秒循环15次。您可以使用两个具有不同时间的循环。另一种方法是在您的循环中使用选项sleep(毫秒)。如果您使用等待时间的参数,您可以通过自定义算法减少它。例如,第一次运行2000毫秒,第二次运行2000毫秒 -(i * 100ms),等等。 - Maarten Kesselaers
问题在于如果我得到一个很大的数字,我就必须进行计数。我不想减慢速度,而是想加快速度。 - Darren
然后你使用我所说的相反方式: 以1000毫秒为起始时间 第二个时间:1000毫秒 + i * 100毫秒 - Maarten Kesselaers

3

这样你就可以无阻塞睡眠了!将其贴到UILabel上,它会从当前显示的值开始向上或向下计数,清除非数字字符。如有需要,可将其从Double调整为Int或Float。

yourlabel.countAnimation(upto: 100.0)
another simple alternative

extension UILabel {    
    func countAnimation(upto: Double) {
        let from: Double = text?.replace(string: ",", replacement: ".").components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted).first.flatMap { Double($0) } ?? 0.0
        let steps: Int = 20
        let duration = 0.350
        let rate = duration / Double(steps)
        let diff = upto - from
        for i in 0...steps {
            DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(i)) {
                self.text = "\(from + diff * (Double(i) / Double(steps)))"
            }
        }
    }
}

这个问题已经有8年的历史了。 - Darren
1
更简单的解决方案。 - Sergio

1
如果您想要一个快速的计数动画,可以使用递归函数,像这样:
func updateGems(diff: Int) {
     animateIncrement(diff: diff, index: 0, start: 50)
}

func animateIncrement(diff: Int, index: Int, start: Int) {

    if index == diff {return}
    gemsLabel.text = "\(start + index)"
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.002) {
        self.animateIncrement(diff: diff, index: index + 1, start: start)
    }
}

1

以下是我对@Sergio回答的改编:

extension UILabel {
    func countAnimation(upto: Double) {
        let fromString = text?.replacingOccurrences(of: "Score: ", with: "")
        let from: Double = fromString?.replacingOccurrences(of: ",", with: ".")
            .components(separatedBy: CharacterSet.init(charactersIn: "-0123456789.").inverted)
            .first.flatMap { Double($0) } ?? 0.0
        let duration = 0.4
        let diff = upto - from
        let rate = duration / diff
        for item in 0...Int(diff) {
            DispatchQueue.main.asyncAfter(deadline: .now() + rate * Double(item)) {
                self.text = "Score: \(Int(from + diff * (Double(item) / diff)))"
            }
        }
    }
}

所需步骤数量基于起始值和结束值之间的差异。


1

Swift 4 代码:

let animationPeriod: Float = 1
    DispatchQueue.global(qos: .default).async(execute: {
        for i in 1..<Int(endValue) {
            usleep(useconds_t(animationPeriod / 10 * 10000)) // sleep in microseconds
            DispatchQueue.main.async(execute: {
                self.lbl.text = "\(i+1)"
            })
        }
    })

1
对于寻找线性减速计数器(Swift 5)的人们:
    func animateCountLabel(to userCount: Int) {
        countLabel.text = " "
        let countStart: Int = userCount - 10
        let countEnd: Int = userCount
        DispatchQueue.global(qos: .default).async { [weak self] in
            for i in (countStart...countEnd) {
                let delayTime = UInt64(pow(2, (Float(i) - Float(countStart))))
                usleep(useconds_t( delayTime * 1300 )) // sleep in microseconds
                DispatchQueue.main.async { [weak self] in
                    self?.countLabel.text = "\(i)"
                }
            }
        }
    }

你可以通过更改静态值来调整起始和结束点以及减慢速度,以满足你的需求 ;)

0

这是我是如何做到的:

- (void)setupAndStartCounter:(CGFloat)duration {
    NSUInteger step = 3;//use your own logic here to define the step.
    NSUInteger remainder = numberYouWantToCount%step;//for me it was 30

    if (step < remainder) {
        remainder = remainder%step;
    }

    self.aTimer = [self getTimer:[self getInvocation:@selector(animateLabel:increment:) arguments:[NSMutableArray arrayWithObjects:[NSNumber numberWithInteger:remainder], [NSNumber numberWithInteger:step], nil]] timeInterval:(duration * (step/(float) numberYouWantToCountTo)) willRepeat:YES];
    [self addTimerToRunLoop:self.aTimer];
}

- (void)animateLabel:(NSNumber*)remainder increment:(NSNumber*)increment {
    NSInteger finish = finalValue;

    if ([self.aLabel.text integerValue] <= (finish) && ([self.aLabel.text integerValue] + [increment integerValue]<= (finish))) {
        self.aLabel.text = [NSString stringWithFormat:@"%lu",(unsigned long)([self.aLabel.text integerValue] + [increment integerValue])];
    }else{
        self.aLabel.text = [NSString stringWithFormat:@"%lu",(unsigned long)([self.aLabel.text integerValue] + [remainder integerValue])];
        [self.aTimer invalidate];
        self.aTimer = nil;
    }
}

#pragma mark -
#pragma mark Timer related Functions

- (NSTimer*)getTimer:(NSInvocation *)invocation timeInterval:(NSTimeInterval)timeInterval willRepeat:(BOOL)willRepeat
{
    return [NSTimer timerWithTimeInterval:timeInterval invocation:invocation repeats:willRepeat];
}

- (NSInvocation*)getInvocation:(SEL)methodName arguments:(NSMutableArray*)arguments
{
    NSMethodSignature *sig = [self methodSignatureForSelector:methodName];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setTarget:self];
    [invocation setSelector:methodName];
    if (arguments != nil)
    {
        id arg1 = [arguments objectAtIndex:0];
        id arg2 = [arguments objectAtIndex:1];
        [invocation setArgument:&arg1 atIndex:2];
        [invocation setArgument:&arg2 atIndex:3];
    }
    return invocation;
}

- (void)addTimerToRunLoop:(NSTimer*)timer
{
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

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