在Swift中为循环进度视图(计时器)动画



我有此代码用于循环进度视图,它显示了您所在的计时器的进度,例如。您将计时器设置为60秒,当您达到30秒时,进度栏是一半。该代码如下所示:

import UIKit
class TimerCircularProgressView: UIView {
@IBInspectable var timeSeconds:CGFloat = CGFloat(timers.seconds[timers.timerID]) {
    didSet {
        setNeedsDisplay()
    }
}
@IBInspectable var timeLabelString:String = "" {
    didSet {
        setNeedsDisplay()
    }
}

private var startAngle = CGFloat(-90 * Double.pi / 180)
private var endAngle = CGFloat(270 * Double.pi / 180)
override init(frame: CGRect) {
    super.init(frame: frame)
    setup()
}
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setup()
}
private func setup() {
    backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
    // General Declarations
    guard let context = UIGraphicsGetCurrentContext() else {
        return
    }
    // Colour Declarations
    let progressColour = UIColor(red: 1, green: 1, blue: 1, alpha: 1)
    let progressBackgroundColour = UIColor(red: 1, green: 1, blue: 1, alpha: 0.4)
    // let titleColour = UIColor(red: 1, green: 1, blue: 1, alpha: 0.8) // let titleColour = UIColor(red: 1, green: 1, blue: 1, alpha: 0.8)
    // Shadow Declarations
    let innerShadow = UIColor.black.withAlphaComponent(0.22)
    let innerShadowOffset = CGSize(width: 3.1, height: 3.1)
    let innerShadowBlurRadius = CGFloat(4)
    // Background Drawing
    let backgroundPath = UIBezierPath(ovalIn: CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height))
    backgroundColor?.setFill()
    backgroundPath.fill()
    // ProgressBackground Drawing
    let progressPadding = CGFloat(timers.deviceWidth / 32 * 1.5) // 15
    print(progressPadding)
    print(timers.deviceWidth)
    let progressBackgroundPath = UIBezierPath(ovalIn: CGRect(x: rect.minX + progressPadding/2, y: rect.minY + progressPadding/2, width: rect.size.width - progressPadding, height: rect.size.height - progressPadding))
    progressBackgroundColour.setStroke()
    progressBackgroundPath.lineWidth = timers.deviceWidth / 32 // 5, 10
    progressBackgroundPath.stroke()
    // Progress Drawing
    let progressRect = CGRect(x: rect.minX + progressPadding/2, y: rect.minY + progressPadding/2, width: rect.size.width - progressPadding, height: rect.size.height - progressPadding)
    let progressPath = UIBezierPath()
    progressPath.addArc(withCenter: CGPoint(x: progressRect.midX, y: progressRect.midY), radius: progressRect.width / 2, startAngle: startAngle, endAngle: (endAngle - startAngle) * (CGFloat(timers.seconds[timers.timerID]/timers.seconds[timers.timerID])  - (timeSeconds / CGFloat(timers.seconds[timers.timerID]))) + startAngle, clockwise: true)
    progressColour.setStroke()
    progressPath.lineWidth = timers.deviceWidth / 32 * 0.8 // Thickness of the progress line
    progressPath.lineCapStyle = CGLineCap.round
    progressPath.stroke()
    // Text Drawing
    let textRect = CGRect(x: rect.minX, y: rect.minY, width: rect.size.width, height: rect.size.height)
    let textContent = NSString(string: timeLabelString)
    let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
    textStyle.alignment = .center
    let textFontAttributes = [
        NSAttributedStringKey.font: UIFont(name: "Helvetica", size: rect.width / timers.timerFontSize)!, // 3, HelveticaNeue-Light
        NSAttributedStringKey.foregroundColor: UIColor.black, // NSAttributedStringKey.foregroundColor: titleColor,
        NSAttributedStringKey.paragraphStyle: textStyle]
    let textHeight = textContent.boundingRect(with: CGSize(width: textRect.width, height: textRect.height), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
    context.saveGState()
    context.clip(to: textRect)
    textContent.draw(in: CGRect(x: textRect.minX, y: textRect.minY + (textRect.height - textHeight) / 2, width: textRect.width, height: textHeight), withAttributes: textFontAttributes)
    context.restoreGState();
}
}

当前,循环进度条每秒更新一次,这意味着在短时间内,进度部分的动画移动是跳跃的,且不光滑。

我尝试设置一个大约0.05秒间隔的计时器,但这可能会给时间不正确,因此我尝试了0.1和0.2,但仍然看起来很跳跃。

@objc func processSlider() {
    if secondsDDecimal > 0 {
        secondsDDecimal -= 0.2
    }
    circularProgressView.timeSeconds = CGFloat(secondsDDecimal)
}

有没有一种方法可以使圆形进度的过渡第二次换一个,以便它光滑而不是跳跃?

您实际上不需要计时器。如果您使用CashApelayer,则可以使strokestart或中风动画,动画系统将为您处理所有人。我有一个示例游乐场在我的github上显示此内容,或者您可以在下面复制和粘贴:

//: Playground - noun: a place where people can play
import UIKit
import PlaygroundSupport

class AnimatedRingView: UIView {
    private static let animationDuration = CFTimeInterval(2)
    private let π = CGFloat.pi
    private let startAngle = 1.5 * CGFloat.pi
    private let strokeWidth = CGFloat(8)
    var proportion = CGFloat(0.5) {
        didSet {
            setNeedsLayout()
        }
    }
    private lazy var circleLayer: CAShapeLayer = {
        let circleLayer = CAShapeLayer()
        circleLayer.strokeColor = UIColor.gray.cgColor
        circleLayer.fillColor = UIColor.clear.cgColor
        circleLayer.lineWidth = self.strokeWidth
        self.layer.addSublayer(circleLayer)
        return circleLayer
    }()
    private lazy var ringlayer: CAShapeLayer = {
        let ringlayer = CAShapeLayer()
        ringlayer.fillColor = UIColor.clear.cgColor
        ringlayer.strokeColor = self.tintColor.cgColor
        ringlayer.lineCap = kCALineCapRound
        ringlayer.lineWidth = self.strokeWidth
        self.layer.addSublayer(ringlayer)
        return ringlayer
    }()
    override func layoutSubviews() {
        super.layoutSubviews()
        let radius = (min(frame.size.width, frame.size.height) - strokeWidth - 2)/2
        let circlePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: startAngle + 2 * π, clockwise: true)
        circleLayer.path = circlePath.cgPath
        ringlayer.path = circlePath.cgPath
        ringlayer.strokeEnd = proportion
    }
    func animateRing(From startProportion: CGFloat, To endProportion: CGFloat, Duration duration: CFTimeInterval = animationDuration) {
        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = duration
        animation.fromValue = startProportion
        animation.toValue = endProportion
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
        ringlayer.strokeEnd = endProportion
        ringlayer.strokeStart = startProportion
        ringlayer.add(animation, forKey: "animateRing")
    }
}
let view = AnimatedRingView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
PlaygroundPage.current.liveView = view
view.animateRing(From: 0, To: 1)
PlaygroundPage.current.needsIndefiniteExecution = true

不久前,我创建了SrcountDownTimer,它确实可以实现您要实现的目标。这是我在那里实施计时器的方式:

private let fireInterval: TimeInterval = 0.01 // ~60 FPS
/**
 * Starts the timer and the animation. If timer was previously runned, it'll invalidate it.
 * - Parameters:
 *   - beginingValue: Value to start countdown from.
 */
public func start(beginingValue: Int) {
    totalTime = TimeInterval(beginingValue)
    elapsedTime = 0
    timer?.invalidate()
    timer = Timer(timeInterval: fireInterval, repeats: true) { [unowned self] _ in
        self.elapsedTime += self.fireInterval
        if self.elapsedTime < self.totalTime {
            self.setNeedsDisplay() // Will invoke "draw" method to update the progress view
        } else {
            self.end()
        }
    }
    RunLoop.main.add(timer!, forMode: .defaultRunLoopMode)
    delegate?.timerDidStart?()
}

基本上,每次计时器发射时,我都会将触发间隔值添加到我的 eLapsedTime 中。这使倒计时用60 fps顺利进行。

在评论中建议使用 timeIntervalsInceNow 方法在> date> date> date 对象中使用其他选项,以通过从当前一个中减去计时器的开始时间戳来计算经过的时间。即使此选项似乎给您更准确的结果,我也不建议您使用它。

原因是,在这种情况下,您将每次计时器触发时计算当前时间戳,这意味着每0.01秒(!)。这是太复杂的解决方案,并且在计时器火灾的间隔不相同的时间间隔100%的时间内接受权衡要聪明得多(但是,差异足以使用户不明显)。

最新更新