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

时间:2017-11-25 19:53:43

标签: ios swift xcode animation timer

我已经获得了循环进度视图的代码,它显示了您通过计时器的距离的进度,例如:您将计时器设置为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)

}

有没有办法让循环进度视图的转换逐秒动画,以便它是平滑的而不是跳跃的?

2 个答案:

答案 0 :(得分:0)

前段时间我创建了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 方法,通过从当前值中减去计时器的开始时间戳来计算已用时间。尽管此选项似乎可以为您提供更准确的结果,但我不建议您使用它。

原因在于,在这种情况下,您将在每次计时器触发时计算当前时间戳,这意味着每0.01秒(!)。这是一个过于复杂的解决方案,并且更加聪明的是接受与100%相同的计时器间隔时间间隔的交易(但差异足够小以至于用户不明显)。

答案 1 :(得分:0)

你实际上并不需要计时器。如果您使用CAShapeLayer,您可以jsut为strokeStart或strokeEnd设置动画,动画系统将为您处理每个标志。我在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