Compare commits
22 Commits
1.2
...
1.5.0-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
| 69a0cd0640 | |||
| 840ae1fcbc | |||
| d96c0a95a3 | |||
| e9d2ffa247 | |||
| 0b46df2ce7 | |||
| e5b6e5caf9 | |||
| fee2df5cfd | |||
| 5726166d3d | |||
| 55f1893864 | |||
| bf388402fd | |||
| f814654106 | |||
| eeacb59a55 | |||
| ee5890b8ce | |||
| d116d7c380 | |||
| 2ec2d6f783 | |||
| 02dccb0804 | |||
| 74c2c3c8d7 | |||
| f9194ec712 | |||
| 54e24f1053 | |||
| 2385466f70 | |||
| ab6a4fd165 | |||
| 5fe36abfe2 |
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'KDCircularProgress'
|
||||
s.version = '1.1'
|
||||
s.version = '1.4.0'
|
||||
s.license = 'MIT'
|
||||
s.summary = 'A circular progress view with gradients written in Swift'
|
||||
s.homepage = 'https://github.com/kaandedeoglu/KDCircularProgress'
|
||||
@@ -11,4 +11,4 @@ Pod::Spec.new do |s|
|
||||
|
||||
s.source_files = 'KDCircularProgress/*.swift'
|
||||
s.requires_arc = true
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,24 +9,20 @@
|
||||
import UIKit
|
||||
|
||||
public enum KDCircularProgressGlowMode {
|
||||
case Forward, Reverse, Constant, NoGlow
|
||||
case forward, reverse, constant, noGlow
|
||||
}
|
||||
|
||||
@IBDesignable
|
||||
public class KDCircularProgress: UIView {
|
||||
|
||||
private struct ConversionFunctions {
|
||||
static func DegreesToRadians (value:CGFloat) -> CGFloat {
|
||||
return value * CGFloat(M_PI) / 180.0
|
||||
}
|
||||
|
||||
static func RadiansToDegrees (value:CGFloat) -> CGFloat {
|
||||
return value * 180.0 / CGFloat(M_PI)
|
||||
private struct Conversion {
|
||||
static func degreesToRadians (value:CGFloat) -> CGFloat {
|
||||
return value * CGFloat.pi / 180.0
|
||||
}
|
||||
}
|
||||
|
||||
private struct UtilityFunctions {
|
||||
static func Clamp<T: Comparable>(value: T, minMax: (T, T)) -> T {
|
||||
private struct Utility {
|
||||
static func clamp<T: Comparable>(value: T, minMax: (T, T)) -> T {
|
||||
let (min, max) = minMax
|
||||
if value < min {
|
||||
return min
|
||||
@@ -37,20 +33,42 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
static func Mod(value: Int, range: Int, minMax: (Int, Int)) -> Int {
|
||||
static func inverseLerp(value: CGFloat, minMax: (CGFloat, CGFloat)) -> CGFloat {
|
||||
return (value - minMax.0) / (minMax.1 - minMax.0)
|
||||
}
|
||||
|
||||
static func lerp(value: CGFloat, minMax: (CGFloat, CGFloat)) -> CGFloat {
|
||||
return (minMax.1 - minMax.0) * value + minMax.0
|
||||
}
|
||||
|
||||
static func colorLerp(value: CGFloat, minMax: (UIColor, UIColor)) -> UIColor {
|
||||
let clampedValue = clamp(value: value, minMax: (0, 1))
|
||||
|
||||
let zero: CGFloat = 0
|
||||
|
||||
var (r0, g0, b0, a0) = (zero, zero, zero, zero)
|
||||
minMax.0.getRed(&r0, green: &g0, blue: &b0, alpha: &a0)
|
||||
|
||||
var (r1, g1, b1, a1) = (zero, zero, zero, zero)
|
||||
minMax.1.getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
|
||||
|
||||
return UIColor(red: lerp(value: clampedValue, minMax: (r0, r1)), green: lerp(value: clampedValue, minMax: (g0, g1)), blue: lerp(value: clampedValue, minMax: (b0, b1)), alpha: lerp(value: clampedValue, minMax: (a0, a1)))
|
||||
}
|
||||
|
||||
static func mod(value: Double, range: Double, minMax: (Double, Double)) -> Double {
|
||||
let (min, max) = minMax
|
||||
assert(abs(range) <= abs(max - min), "range should be <= than the interval")
|
||||
if value >= min && value <= max {
|
||||
return value
|
||||
} else if value < min {
|
||||
return Mod(value + range, range: range, minMax: minMax)
|
||||
return mod(value: value + range, range: range, minMax: minMax)
|
||||
} else {
|
||||
return Mod(value - range, range: range, minMax: minMax)
|
||||
return mod(value: value - range, range: range, minMax: minMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var progressLayer: KDCircularProgressViewLayer! {
|
||||
private var progressLayer: KDCircularProgressViewLayer {
|
||||
get {
|
||||
return layer as! KDCircularProgressViewLayer
|
||||
}
|
||||
@@ -62,7 +80,7 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var angle: Int = 0 {
|
||||
@IBInspectable public var angle: Double = 0 {
|
||||
didSet {
|
||||
if self.isAnimating() {
|
||||
self.pauseAnimation()
|
||||
@@ -71,9 +89,10 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var startAngle: Int = 0 {
|
||||
@IBInspectable public var startAngle: Double = 0 {
|
||||
didSet {
|
||||
progressLayer.startAngle = UtilityFunctions.Mod(startAngle, range: 360, minMax: (0,360))
|
||||
startAngle = Utility.mod(value: startAngle, range: 360, minMax: (0, 360))
|
||||
progressLayer.startAngle = startAngle
|
||||
progressLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
@@ -91,6 +110,12 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var lerpColorMode: Bool = false {
|
||||
didSet {
|
||||
progressLayer.lerpColorMode = lerpColorMode
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var gradientRotateSpeed: CGFloat = 0 {
|
||||
didSet {
|
||||
progressLayer.gradientRotateSpeed = gradientRotateSpeed
|
||||
@@ -99,11 +124,12 @@ public class KDCircularProgress: UIView {
|
||||
|
||||
@IBInspectable public var glowAmount: CGFloat = 1.0 {//Between 0 and 1
|
||||
didSet {
|
||||
progressLayer.glowAmount = UtilityFunctions.Clamp(glowAmount, minMax: (0, 1))
|
||||
glowAmount = Utility.clamp(value: glowAmount, minMax: (0, 1))
|
||||
progressLayer.glowAmount = glowAmount
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var glowMode: KDCircularProgressGlowMode = .Forward {
|
||||
@IBInspectable public var glowMode: KDCircularProgressGlowMode = .forward {
|
||||
didSet {
|
||||
progressLayer.glowMode = glowMode
|
||||
}
|
||||
@@ -111,19 +137,19 @@ public class KDCircularProgress: UIView {
|
||||
|
||||
@IBInspectable public var progressThickness: CGFloat = 0.4 {//Between 0 and 1
|
||||
didSet {
|
||||
progressThickness = UtilityFunctions.Clamp(progressThickness, minMax: (0, 1))
|
||||
progressThickness = Utility.clamp(value: progressThickness, minMax: (0, 1))
|
||||
progressLayer.progressThickness = progressThickness/2
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var trackThickness: CGFloat = 0.5 {//Between 0 and 1
|
||||
didSet {
|
||||
trackThickness = UtilityFunctions.Clamp(trackThickness, minMax: (0, 1))
|
||||
trackThickness = Utility.clamp(value: trackThickness, minMax: (0, 1))
|
||||
progressLayer.trackThickness = trackThickness/2
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var trackColor: UIColor = UIColor.blackColor() {
|
||||
@IBInspectable public var trackColor: UIColor = .black() {
|
||||
didSet {
|
||||
progressLayer.trackColor = trackColor
|
||||
progressLayer.setNeedsDisplay()
|
||||
@@ -135,7 +161,7 @@ public class KDCircularProgress: UIView {
|
||||
if let color = progressInsideFillColor {
|
||||
progressLayer.progressInsideFillColor = color
|
||||
} else {
|
||||
progressLayer.progressInsideFillColor = UIColor.clearColor()
|
||||
progressLayer.progressInsideFillColor = .clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +172,7 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
set(newValue) {
|
||||
setColors(newValue)
|
||||
setColors(colors: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,12 +182,11 @@ public class KDCircularProgress: UIView {
|
||||
@objc @IBInspectable private var IBColor2: UIColor?
|
||||
@objc @IBInspectable private var IBColor3: UIColor?
|
||||
|
||||
|
||||
private var animationCompletionBlock: ((Bool) -> Void)?
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
userInteractionEnabled = false
|
||||
isUserInteractionEnabled = false
|
||||
setInitialValues()
|
||||
refreshValues()
|
||||
checkAndSetIBColors()
|
||||
@@ -169,38 +194,44 @@ public class KDCircularProgress: UIView {
|
||||
|
||||
convenience public init(frame:CGRect, colors: UIColor...) {
|
||||
self.init(frame: frame)
|
||||
setColors(colors)
|
||||
setColors(colors: colors)
|
||||
}
|
||||
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)!
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
userInteractionEnabled = false
|
||||
setInitialValues()
|
||||
super.init(coder: aDecoder)!
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
isUserInteractionEnabled = false
|
||||
setInitialValues()
|
||||
refreshValues()
|
||||
}
|
||||
}
|
||||
|
||||
public override func awakeFromNib() {
|
||||
checkAndSetIBColors()
|
||||
}
|
||||
|
||||
|
||||
override public class func layerClass() -> AnyClass {
|
||||
return KDCircularProgressViewLayer.self
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
radius = (frame.size.width/2.0) * 0.8
|
||||
}
|
||||
|
||||
private func setInitialValues() {
|
||||
radius = (frame.size.width/2.0) * 0.8 //We always apply a 20% padding, stopping glows from being clipped
|
||||
backgroundColor = .clearColor()
|
||||
setColors(UIColor.whiteColor(), UIColor.redColor())
|
||||
backgroundColor = .clear()
|
||||
setColors(colors: .white(), .cyan())
|
||||
}
|
||||
|
||||
private func refreshValues() {
|
||||
progressLayer.angle = angle
|
||||
progressLayer.startAngle = UtilityFunctions.Mod(startAngle, range: 360, minMax: (0,360))
|
||||
progressLayer.startAngle = startAngle
|
||||
progressLayer.clockwise = clockwise
|
||||
progressLayer.roundedCorners = roundedCorners
|
||||
progressLayer.lerpColorMode = lerpColorMode
|
||||
progressLayer.gradientRotateSpeed = gradientRotateSpeed
|
||||
progressLayer.glowAmount = UtilityFunctions.Clamp(glowAmount, minMax: (0, 1))
|
||||
progressLayer.glowAmount = glowAmount
|
||||
progressLayer.glowMode = glowMode
|
||||
progressLayer.progressThickness = progressThickness/2
|
||||
progressLayer.trackColor = trackColor
|
||||
@@ -208,14 +239,14 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
private func checkAndSetIBColors() {
|
||||
let nonNilColors = [IBColor1, IBColor2, IBColor3].filter { $0 != nil}.map { $0! }
|
||||
if nonNilColors.count > 0 {
|
||||
setColors(nonNilColors)
|
||||
let nonNilColors = [IBColor1, IBColor2, IBColor3].flatMap { $0 }
|
||||
if !nonNilColors.isEmpty {
|
||||
setColors(colors: nonNilColors)
|
||||
}
|
||||
}
|
||||
|
||||
public func setColors(colors: UIColor...) {
|
||||
setColors(colors)
|
||||
setColors(colors: colors)
|
||||
}
|
||||
|
||||
private func setColors(colors: [UIColor]) {
|
||||
@@ -223,31 +254,41 @@ public class KDCircularProgress: UIView {
|
||||
progressLayer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func animateFromAngle(fromAngle: Int, toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?) {
|
||||
public func animate(fromAngle: Double, toAngle: Double, duration: TimeInterval, relativeDuration: Bool = true, completion: ((Bool) -> Void)?) {
|
||||
if isAnimating() {
|
||||
pauseAnimation()
|
||||
}
|
||||
|
||||
|
||||
let animationDuration: TimeInterval
|
||||
if relativeDuration {
|
||||
animationDuration = duration
|
||||
} else {
|
||||
let traveledAngle = Utility.mod(value: toAngle - fromAngle, range: 360, minMax: (0, 360))
|
||||
let scaledDuration = (TimeInterval(traveledAngle) * duration) / 360
|
||||
animationDuration = scaledDuration
|
||||
}
|
||||
|
||||
let animation = CABasicAnimation(keyPath: "angle")
|
||||
animation.fromValue = fromAngle
|
||||
animation.toValue = toAngle
|
||||
animation.duration = duration
|
||||
animation.duration = animationDuration
|
||||
animation.delegate = self
|
||||
angle = toAngle
|
||||
animationCompletionBlock = completion
|
||||
|
||||
progressLayer.addAnimation(animation, forKey: "angle")
|
||||
progressLayer.add(animation, forKey: "angle")
|
||||
}
|
||||
|
||||
public func animateToAngle(toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?) {
|
||||
public func animate(toAngle: Double, duration: TimeInterval, relativeDuration: Bool = true, completion: ((Bool) -> Void)?) {
|
||||
if isAnimating() {
|
||||
pauseAnimation()
|
||||
}
|
||||
animateFromAngle(angle, toAngle: toAngle, duration: duration, completion: completion)
|
||||
animate(fromAngle: angle, toAngle: toAngle, duration: duration, relativeDuration: relativeDuration, completion: completion)
|
||||
}
|
||||
|
||||
public func pauseAnimation() {
|
||||
let presentationLayer = progressLayer.presentationLayer() as! KDCircularProgressViewLayer
|
||||
guard let presentationLayer = progressLayer.presentation() else { return }
|
||||
|
||||
let currentValue = presentationLayer.angle
|
||||
progressLayer.removeAllAnimations()
|
||||
animationCompletionBlock = nil
|
||||
@@ -255,15 +296,16 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
public func stopAnimation() {
|
||||
animationCompletionBlock = nil
|
||||
progressLayer.removeAllAnimations()
|
||||
angle = 0
|
||||
}
|
||||
|
||||
public func isAnimating() -> Bool {
|
||||
return progressLayer.animationForKey("angle") != nil
|
||||
return progressLayer.animation(forKey: "angle") != nil
|
||||
}
|
||||
|
||||
override public func animationDidStop(anim: CAAnimation, finished flag: Bool) {
|
||||
override public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
||||
if let completionBlock = animationCompletionBlock {
|
||||
completionBlock(flag)
|
||||
animationCompletionBlock = nil
|
||||
@@ -276,7 +318,7 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public override func willMoveToSuperview(newSuperview: UIView?) {
|
||||
public override func willMove(toSuperview newSuperview: UIView?) {
|
||||
if newSuperview == nil && isAnimating() {
|
||||
pauseAnimation()
|
||||
}
|
||||
@@ -290,36 +332,50 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
private class KDCircularProgressViewLayer: CALayer {
|
||||
@NSManaged var angle: Int
|
||||
var radius: CGFloat!
|
||||
var startAngle: Int!
|
||||
var clockwise: Bool!
|
||||
@NSManaged var angle: Double
|
||||
var radius: CGFloat! {
|
||||
didSet {
|
||||
invalidateGradientCache()
|
||||
}
|
||||
}
|
||||
var startAngle: Double!
|
||||
var clockwise: Bool! {
|
||||
didSet {
|
||||
if clockwise != oldValue {
|
||||
invalidateGradientCache()
|
||||
}
|
||||
}
|
||||
}
|
||||
var roundedCorners: Bool!
|
||||
var gradientRotateSpeed: CGFloat!
|
||||
var lerpColorMode: Bool!
|
||||
var gradientRotateSpeed: CGFloat! {
|
||||
didSet {
|
||||
invalidateGradientCache()
|
||||
}
|
||||
}
|
||||
var glowAmount: CGFloat!
|
||||
var glowMode: KDCircularProgressGlowMode!
|
||||
var progressThickness: CGFloat!
|
||||
var trackThickness: CGFloat!
|
||||
var trackColor: UIColor!
|
||||
var progressInsideFillColor: UIColor = UIColor.clearColor()
|
||||
var progressInsideFillColor = UIColor.clear()
|
||||
var colorsArray: [UIColor]! {
|
||||
didSet {
|
||||
gradientCache = nil
|
||||
locationsCache = nil
|
||||
invalidateGradientCache()
|
||||
}
|
||||
}
|
||||
var gradientCache: CGGradientRef?
|
||||
var locationsCache: [CGFloat]?
|
||||
private var gradientCache: CGGradient?
|
||||
private var locationsCache: [CGFloat]?
|
||||
|
||||
struct GlowConstants {
|
||||
static let sizeToGlowRatio: CGFloat = 0.00015
|
||||
static func glowAmountForAngle(angle: Int, glowAmount: CGFloat, glowMode: KDCircularProgressGlowMode, size: CGFloat) -> CGFloat {
|
||||
private struct GlowConstants {
|
||||
private static let sizeToGlowRatio: CGFloat = 0.00015
|
||||
static func glowAmount(forAngle angle: Double, glowAmount: CGFloat, glowMode: KDCircularProgressGlowMode, size: CGFloat) -> CGFloat {
|
||||
switch glowMode {
|
||||
case .Forward:
|
||||
case .forward:
|
||||
return CGFloat(angle) * size * sizeToGlowRatio * glowAmount
|
||||
case .Reverse:
|
||||
case .reverse:
|
||||
return CGFloat(360 - angle) * size * sizeToGlowRatio * glowAmount
|
||||
case .Constant:
|
||||
case .constant:
|
||||
return 360 * size * sizeToGlowRatio * glowAmount
|
||||
default:
|
||||
return 0
|
||||
@@ -327,8 +383,8 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override class func needsDisplayForKey(key: String) -> Bool {
|
||||
return key == "angle" ? true : super.needsDisplayForKey(key)
|
||||
override class func needsDisplay(forKey key: String) -> Bool {
|
||||
return key == "angle" ? true : super.needsDisplay(forKey: key)
|
||||
}
|
||||
|
||||
override init(layer: AnyObject) {
|
||||
@@ -339,6 +395,7 @@ public class KDCircularProgress: UIView {
|
||||
startAngle = progressLayer.startAngle
|
||||
clockwise = progressLayer.clockwise
|
||||
roundedCorners = progressLayer.roundedCorners
|
||||
lerpColorMode = progressLayer.lerpColorMode
|
||||
gradientRotateSpeed = progressLayer.gradientRotateSpeed
|
||||
glowAmount = progressLayer.glowAmount
|
||||
glowMode = progressLayer.glowMode
|
||||
@@ -346,128 +403,160 @@ public class KDCircularProgress: UIView {
|
||||
trackThickness = progressLayer.trackThickness
|
||||
trackColor = progressLayer.trackColor
|
||||
colorsArray = progressLayer.colorsArray
|
||||
progressInsideFillColor = progressLayer.progressInsideFillColor
|
||||
}
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
override func drawInContext(ctx: CGContext) {
|
||||
override func draw(in ctx: CGContext) {
|
||||
UIGraphicsPushContext(ctx)
|
||||
let rect = bounds
|
||||
let size = rect.size
|
||||
|
||||
let trackLineWidth: CGFloat = radius * trackThickness
|
||||
let size = bounds.size
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
|
||||
let trackLineWidth = radius * trackThickness
|
||||
let progressLineWidth = radius * progressThickness
|
||||
let arcRadius = max(radius - trackLineWidth/2, radius - progressLineWidth/2)
|
||||
CGContextAddArc(ctx, CGFloat(size.width/2.0), CGFloat(size.height/2.0), arcRadius, 0, CGFloat(M_PI * 2), 0)
|
||||
ctx.addArc(centerX: width/2.0, y: height/2.0, radius: arcRadius, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: 0)
|
||||
trackColor.set()
|
||||
CGContextSetStrokeColorWithColor(ctx, trackColor.CGColor)
|
||||
CGContextSetFillColorWithColor(ctx, progressInsideFillColor.CGColor)
|
||||
CGContextSetLineWidth(ctx, trackLineWidth)
|
||||
CGContextSetLineCap(ctx, CGLineCap.Butt)
|
||||
CGContextDrawPath(ctx, .FillStroke)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
let imageCtx = UIGraphicsGetCurrentContext()
|
||||
let reducedAngle = UtilityFunctions.Mod(angle, range: 360, minMax: (0, 360))
|
||||
let fromAngle = ConversionFunctions.DegreesToRadians(CGFloat(-startAngle))
|
||||
let toAngle = ConversionFunctions.DegreesToRadians(CGFloat((clockwise == true ? -reducedAngle : reducedAngle) - startAngle))
|
||||
CGContextAddArc(imageCtx, CGFloat(size.width/2.0),CGFloat(size.height/2.0), arcRadius, fromAngle, toAngle, clockwise == true ? 1 : 0)
|
||||
let glowValue = GlowConstants.glowAmountForAngle(reducedAngle, glowAmount: glowAmount, glowMode: glowMode, size: size.width)
|
||||
if glowValue > 0 {
|
||||
CGContextSetShadowWithColor(imageCtx, CGSizeZero, glowValue, UIColor.blackColor().CGColor)
|
||||
}
|
||||
CGContextSetLineCap(imageCtx, roundedCorners == true ? .Round : .Butt)
|
||||
CGContextSetLineWidth(imageCtx, progressLineWidth)
|
||||
CGContextDrawPath(imageCtx, .Stroke)
|
||||
ctx.setStrokeColor(trackColor.cgColor)
|
||||
ctx.setFillColor(progressInsideFillColor.cgColor)
|
||||
ctx.setLineWidth(trackLineWidth)
|
||||
ctx.setLineCap(CGLineCap.butt)
|
||||
ctx.drawPath(using: .fillStroke)
|
||||
|
||||
let drawMask: CGImageRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext())!
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
|
||||
let imageCtx = UIGraphicsGetCurrentContext()
|
||||
let reducedAngle = Utility.mod(value: angle, range: 360, minMax: (0, 360))
|
||||
let fromAngle = Conversion.degreesToRadians(value: CGFloat(-startAngle))
|
||||
let toAngle = Conversion.degreesToRadians(value: CGFloat((clockwise == true ? -reducedAngle : reducedAngle) - startAngle))
|
||||
|
||||
imageCtx?.addArc(centerX: width/2.0, y: height/2.0, radius: arcRadius, startAngle: fromAngle, endAngle: toAngle, clockwise: clockwise == true ? 1 : 0)
|
||||
|
||||
let glowValue = GlowConstants.glowAmount(forAngle: reducedAngle, glowAmount: glowAmount, glowMode: glowMode, size: width)
|
||||
if glowValue > 0 {
|
||||
imageCtx?.setShadow(offset: CGSize.zero, blur: glowValue, color: UIColor.black().cgColor)
|
||||
}
|
||||
|
||||
let linecap: CGLineCap = roundedCorners == true ? .round : .butt
|
||||
imageCtx?.setLineCap(linecap)
|
||||
imageCtx?.setLineWidth(progressLineWidth)
|
||||
imageCtx?.drawPath(using: .stroke)
|
||||
|
||||
let drawMask: CGImage = UIGraphicsGetCurrentContext()!.makeImage()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
CGContextSaveGState(ctx)
|
||||
CGContextClipToMask(ctx, bounds, drawMask)
|
||||
ctx.saveGState()
|
||||
ctx.clipToMask(bounds, mask: drawMask)
|
||||
|
||||
//Gradient - Fill
|
||||
if colorsArray.count > 1 {
|
||||
var componentsArray: [CGFloat] = []
|
||||
let rgbColorsArray: [UIColor] = colorsArray.map {c in // Make sure every color in colors array is in RGB color space
|
||||
if CGColorGetNumberOfComponents(c.CGColor) == 2 {
|
||||
let whiteValue = CGColorGetComponents(c.CGColor)[0]
|
||||
return UIColor(red: whiteValue, green: whiteValue, blue: whiteValue, alpha: 1.0)
|
||||
if !lerpColorMode && colorsArray.count > 1 {
|
||||
let rgbColorsArray: [UIColor] = colorsArray.map { color in // Make sure every color in colors array is in RGB color space
|
||||
if color.cgColor.numberOfComponents == 2 {
|
||||
if let whiteValue = color.cgColor.components?[0] {
|
||||
return UIColor(red: whiteValue, green: whiteValue, blue: whiteValue, alpha: 1.0)
|
||||
} else {
|
||||
return UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
|
||||
}
|
||||
} else {
|
||||
return c
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
for color in rgbColorsArray {
|
||||
let colorComponents: UnsafePointer<CGFloat> = CGColorGetComponents(color.CGColor)
|
||||
componentsArray.appendContentsOf([colorComponents[0],colorComponents[1],colorComponents[2],1.0])
|
||||
let componentsArray = rgbColorsArray.flatMap { color -> [CGFloat] in
|
||||
let components: UnsafePointer<CGFloat> = color.cgColor.components!
|
||||
return [components[0], components[1], components[2], 1.0]
|
||||
}
|
||||
|
||||
drawGradientWithContext(ctx, componentsArray: componentsArray)
|
||||
drawGradientWith(context: ctx, componentsArray: componentsArray)
|
||||
} else {
|
||||
if colorsArray.count == 1 {
|
||||
fillRectWithContext(ctx, color: colorsArray[0])
|
||||
var color: UIColor?
|
||||
if colorsArray.isEmpty {
|
||||
color = UIColor.white()
|
||||
} else if colorsArray.count == 1 {
|
||||
color = colorsArray[0]
|
||||
} else {
|
||||
fillRectWithContext(ctx, color: UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0))
|
||||
// lerpColorMode is true
|
||||
let t = CGFloat(reducedAngle) / 360
|
||||
let steps = colorsArray.count - 1
|
||||
let step = 1 / CGFloat(steps)
|
||||
for i in 1...steps {
|
||||
let fi = CGFloat(i)
|
||||
if (t <= fi * step || i == steps) {
|
||||
let colorT = Utility.inverseLerp(value: t, minMax: ((fi - 1) * step, fi * step))
|
||||
color = Utility.colorLerp(value: colorT, minMax: (colorsArray[i - 1], colorsArray[i]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let color = color {
|
||||
fillRectWith(context: ctx, color: color)
|
||||
}
|
||||
}
|
||||
CGContextRestoreGState(ctx)
|
||||
ctx.restoreGState()
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
func fillRectWithContext(ctx: CGContext!, color: UIColor) {
|
||||
CGContextSetFillColorWithColor(ctx, color.CGColor)
|
||||
CGContextFillRect(ctx, bounds)
|
||||
private func fillRectWith(context: CGContext!, color: UIColor) {
|
||||
context.setFillColor(color.cgColor)
|
||||
context.fill(bounds)
|
||||
}
|
||||
|
||||
func drawGradientWithContext(ctx: CGContext!, componentsArray: [CGFloat]) {
|
||||
private func drawGradientWith(context: CGContext!, componentsArray: [CGFloat]) {
|
||||
let baseSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let locations = locationsCache ?? gradientLocationsFromColorCount(componentsArray.count/4, gradientWidth: bounds.size.width)
|
||||
let locations = locationsCache ?? gradientLocationsFor(colorCount: componentsArray.count/4, gradientWidth: bounds.size.width)
|
||||
let gradient: CGGradient
|
||||
|
||||
if let g = self.gradientCache {
|
||||
gradient = g
|
||||
|
||||
if let cachedGradient = gradientCache {
|
||||
gradient = cachedGradient
|
||||
} else {
|
||||
let g = CGGradientCreateWithColorComponents(baseSpace, componentsArray, locations,componentsArray.count / 4)
|
||||
self.gradientCache = g
|
||||
gradient = g!
|
||||
guard let cachedGradient = CGGradient(colorComponentsSpace: baseSpace, components: componentsArray, locations: locations,count: componentsArray.count / 4) else {
|
||||
return
|
||||
}
|
||||
|
||||
gradientCache = cachedGradient
|
||||
gradient = cachedGradient
|
||||
}
|
||||
|
||||
let halfX = bounds.size.width/2.0
|
||||
let floatPi = CGFloat(M_PI)
|
||||
let halfX = bounds.size.width / 2.0
|
||||
let floatPi = CGFloat.pi
|
||||
let rotateSpeed = clockwise == true ? gradientRotateSpeed : gradientRotateSpeed * -1
|
||||
let angleInRadians = ConversionFunctions.DegreesToRadians(rotateSpeed * CGFloat(angle) - 90)
|
||||
let angleInRadians = Conversion.degreesToRadians(value: rotateSpeed! * CGFloat(angle) - 90)
|
||||
let oppositeAngle = angleInRadians > floatPi ? angleInRadians - floatPi : angleInRadians + floatPi
|
||||
|
||||
let startPoint = CGPoint(x: (cos(angleInRadians) * halfX) + halfX, y: (sin(angleInRadians) * halfX) + halfX)
|
||||
let endPoint = CGPoint(x: (cos(oppositeAngle) * halfX) + halfX, y: (sin(oppositeAngle) * halfX) + halfX)
|
||||
|
||||
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, .DrawsBeforeStartLocation)
|
||||
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsBeforeStartLocation)
|
||||
}
|
||||
|
||||
func gradientLocationsFromColorCount(colorCount: Int, gradientWidth: CGFloat) -> [CGFloat] {
|
||||
private func gradientLocationsFor(colorCount: Int, gradientWidth: CGFloat) -> [CGFloat] {
|
||||
if colorCount == 0 || gradientWidth == 0 {
|
||||
return []
|
||||
} else {
|
||||
var locationsArray: [CGFloat] = []
|
||||
let progressLineWidth = radius * progressThickness
|
||||
let firstPoint = gradientWidth/2 - (radius - progressLineWidth/2)
|
||||
let increment = (gradientWidth - (2*firstPoint))/CGFloat(colorCount - 1)
|
||||
|
||||
for i in 0..<colorCount {
|
||||
locationsArray.append(firstPoint + (CGFloat(i) * increment))
|
||||
}
|
||||
assert(locationsArray.count == colorCount, "color counts should be equal")
|
||||
let locationsArray = (0..<colorCount).map { firstPoint + (CGFloat($0) * increment) }
|
||||
let result = locationsArray.map { $0 / gradientWidth }
|
||||
locationsCache = result
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateGradientCache() {
|
||||
gradientCache = nil
|
||||
locationsCache = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,9 +174,11 @@
|
||||
TargetAttributes = {
|
||||
BC9E75851A8CE4A500B1DF3D = {
|
||||
CreatedOnToolsVersion = 6.1.1;
|
||||
LastSwiftMigration = 0800;
|
||||
};
|
||||
BC9E759A1A8CE4A500B1DF3D = {
|
||||
CreatedOnToolsVersion = 6.1.1;
|
||||
LastSwiftMigration = 0800;
|
||||
TestTargetID = BC9E75851A8CE4A500B1DF3D;
|
||||
};
|
||||
};
|
||||
@@ -353,6 +355,7 @@
|
||||
INFOPLIST_FILE = KDCircularProgressExample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -363,6 +366,7 @@
|
||||
INFOPLIST_FILE = KDCircularProgressExample/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -381,6 +385,7 @@
|
||||
INFOPLIST_FILE = KDCircularProgressExampleTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KDCircularProgressExample.app/KDCircularProgressExample";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -396,6 +401,7 @@
|
||||
INFOPLIST_FILE = KDCircularProgressExampleTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 3.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/KDCircularProgressExample.app/KDCircularProgressExample";
|
||||
};
|
||||
name = Release;
|
||||
|
||||
@@ -14,30 +14,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(application: UIApplication) {
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(application: UIApplication) {
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(application: UIApplication) {
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(application: UIApplication) {
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(application: UIApplication) {
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8191" systemVersion="15A279b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="vXZ-lx-hvc">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="8191" systemVersion="15A284" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="vXZ-lx-hvc">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8154"/>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
var progress: KDCircularProgress!
|
||||
|
||||
override func viewDidLoad() {
|
||||
@@ -23,19 +22,19 @@ class ViewController: UIViewController {
|
||||
progress.clockwise = true
|
||||
progress.gradientRotateSpeed = 2
|
||||
progress.roundedCorners = false
|
||||
progress.glowMode = .Forward
|
||||
progress.glowMode = .forward
|
||||
progress.glowAmount = 0.9
|
||||
progress.setColors(UIColor.cyanColor() ,UIColor.whiteColor(), UIColor.magentaColor(), UIColor.whiteColor(), UIColor.orangeColor())
|
||||
progress.setColors(colors: UIColor.cyan() ,UIColor.white(), UIColor.magenta(), UIColor.white(), UIColor.orange())
|
||||
progress.center = CGPoint(x: view.center.x, y: view.center.y + 25)
|
||||
view.addSubview(progress)
|
||||
}
|
||||
|
||||
@IBAction func sliderDidChangeValue(sender: UISlider) {
|
||||
progress.angle = Int(sender.value)
|
||||
@IBAction func sliderDidChangeValue(_ sender: UISlider) {
|
||||
progress.angle = Double(sender.value)
|
||||
}
|
||||
|
||||
@IBAction func animateButtonTapped(sender: UIButton) {
|
||||
progress.animateToAngle(360, duration: 5) { completed in
|
||||
@IBAction func animateButtonTapped(_ sender: UIButton) {
|
||||
progress.animate(fromAngle: 0, toAngle: 360, duration: 5) { completed in
|
||||
if completed {
|
||||
print("animation stopped, completed")
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class KDCircularProgressExampleTests: XCTestCase {
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measureBlock() {
|
||||
self.measure() {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ Here's an example
|
||||
[Youtube Link](http://youtu.be/iIdas72MXOg)
|
||||
|
||||
|
||||
[](http://youtu.be/iIdas72MXOg)
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
@@ -33,7 +34,7 @@ view.addSubview(progress)
|
||||
```
|
||||
|
||||
## Installation
|
||||
- It's on Cocoapods under the name (you guessed it!) KDCircularProgress
|
||||
- It's on CocoaPods under the name (you guessed it!) KDCircularProgress
|
||||
- Just drag `KDCircularProgress.swift` into your project. `Carthage` support is on To-do list.
|
||||
|
||||
## Properties
|
||||
@@ -100,11 +101,13 @@ public func setColors(colors: UIColor...)
|
||||
Set the colors for the progress gradient.
|
||||
|
||||
```swift
|
||||
public func animateFromAngle(fromAngle: Int, toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?)
|
||||
public func animateFromAngle(fromAngle: Int, toAngle: Int, duration: NSTimeInterval, relativeDuration: Bool = true, completion: ((Bool) -> Void)?)
|
||||
```
|
||||
|
||||
Animate the progress from an initial value to a final value, with a completion block that fires after the animation is done.
|
||||
|
||||
`relativeDuration` - specify if the duration is for the specific animation or is the duration that would make a full turn.
|
||||
|
||||
```swift
|
||||
public func animateToAngle(toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?)
|
||||
```
|
||||
@@ -129,7 +132,7 @@ Prefering light colors in the gradients gives better results. As mentioned befor
|
||||
##To-Do
|
||||
- [x] Add example project
|
||||
- [ ] Carthage Support
|
||||
- [x] Cocoapods Support
|
||||
- [x] CocoaPods Support
|
||||
- [x] IBDesignable/IBInspectable support
|
||||
- [ ] Adding a `progress` property as an alternative to `angle`
|
||||
- [ ] Clean up
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 861 KiB |
Reference in New Issue
Block a user