我有此代码用于循环进度视图,它显示了您所在的计时器的进度,例如。您将计时器设置为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%的时间内接受权衡要聪明得多(但是,差异足以使用户不明显)。