Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 094a5a64e5 | |||
| 3c38489154 | |||
| 1d9ab519f1 | |||
| 60d95709c7 | |||
| 0e95379f6d | |||
| 840ae1fcbc | |||
| 36160ed532 | |||
| d96c0a95a3 | |||
| e9d2ffa247 | |||
| 0b46df2ce7 | |||
| e5b6e5caf9 | |||
| fee2df5cfd | |||
| 5726166d3d | |||
| 55f1893864 | |||
| bf388402fd | |||
| f814654106 | |||
| eeacb59a55 | |||
| ee5890b8ce | |||
| d116d7c380 | |||
| 2ec2d6f783 | |||
| 02dccb0804 | |||
| 74c2c3c8d7 | |||
| f9194ec712 | |||
| 54e24f1053 | |||
| 2385466f70 | |||
| ab6a4fd165 | |||
| 5fe36abfe2 | |||
| 466be89f6d | |||
| b9f8a3e46e | |||
| b9cdde76e5 | |||
| f7959fdd54 | |||
| c01c9d9b59 | |||
| 125bfcc5a4 | |||
| e823e29b2e | |||
| ee6a46abd3 | |||
| a90c94254e | |||
| 9fb2796b70 | |||
| 58cbb4a4a6 |
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'KDCircularProgress'
|
||||
s.version = '1.0'
|
||||
s.version = '1.4.1'
|
||||
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
|
||||
|
||||
Regular → Executable
+208
-106
@@ -15,18 +15,18 @@ public enum KDCircularProgressGlowMode {
|
||||
@IBDesignable
|
||||
public class KDCircularProgress: UIView {
|
||||
|
||||
private struct ConversionFunctions {
|
||||
static func DegreesToRadians (value:CGFloat) -> CGFloat {
|
||||
private struct Conversion {
|
||||
static func degreesToRadians (value:CGFloat) -> CGFloat {
|
||||
return value * CGFloat(M_PI) / 180.0
|
||||
}
|
||||
|
||||
static func RadiansToDegrees (value:CGFloat) -> CGFloat {
|
||||
static func radiansToDegrees (value:CGFloat) -> CGFloat {
|
||||
return value * 180.0 / CGFloat(M_PI)
|
||||
}
|
||||
}
|
||||
|
||||
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 +37,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, 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(clampedValue, minMax: (r0, r1)), green: lerp(clampedValue, minMax: (g0, g1)), blue: lerp(clampedValue, minMax: (b0, b1)), alpha: lerp(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 + range, range: range, minMax: minMax)
|
||||
} else {
|
||||
return Mod(value - range, range: range, minMax: minMax)
|
||||
return mod(value - range, range: range, minMax: minMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var progressLayer: KDCircularProgressViewLayer! {
|
||||
private var progressLayer: KDCircularProgressViewLayer {
|
||||
get {
|
||||
return layer as! KDCircularProgressViewLayer
|
||||
}
|
||||
@@ -62,7 +84,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 +93,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(startAngle, range: 360, minMax: (0, 360))
|
||||
progressLayer.startAngle = startAngle
|
||||
progressLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
@@ -91,6 +114,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,7 +128,8 @@ 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(glowAmount, minMax: (0, 1))
|
||||
progressLayer.glowAmount = glowAmount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,25 +141,35 @@ 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(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(trackThickness, minMax: (0, 1))
|
||||
progressLayer.trackThickness = trackThickness/2
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var trackColor: UIColor = UIColor.blackColor() {
|
||||
@IBInspectable public var trackColor: UIColor = .blackColor() {
|
||||
didSet {
|
||||
progressLayer.trackColor = trackColor
|
||||
progressLayer.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var progressInsideFillColor: UIColor? = nil {
|
||||
didSet {
|
||||
if let color = progressInsideFillColor {
|
||||
progressLayer.progressInsideFillColor = color
|
||||
} else {
|
||||
progressLayer.progressInsideFillColor = .clearColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBInspectable public var progressColors: [UIColor]! {
|
||||
get {
|
||||
return progressLayer.colorsArray
|
||||
@@ -142,10 +182,9 @@ public class KDCircularProgress: UIView {
|
||||
|
||||
//These are used only from the Interface-Builder. Changing these from code will have no effect.
|
||||
//Also IB colors are limited to 3, whereas programatically we can have an arbitrary number of them.
|
||||
@IBInspectable public var IBColor1: UIColor?
|
||||
@IBInspectable public var IBColor2: UIColor?
|
||||
@IBInspectable public var IBColor3: UIColor?
|
||||
|
||||
@objc @IBInspectable private var IBColor1: UIColor?
|
||||
@objc @IBInspectable private var IBColor2: UIColor?
|
||||
@objc @IBInspectable private var IBColor3: UIColor?
|
||||
|
||||
private var animationCompletionBlock: ((Bool) -> Void)?
|
||||
|
||||
@@ -161,36 +200,42 @@ public class KDCircularProgress: UIView {
|
||||
self.init(frame: frame)
|
||||
setColors(colors)
|
||||
}
|
||||
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
setTranslatesAutoresizingMaskIntoConstraints(false)
|
||||
userInteractionEnabled = false
|
||||
setInitialValues()
|
||||
super.init(coder: aDecoder)!
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
userInteractionEnabled = 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())
|
||||
setColors(.whiteColor(), .cyanColor())
|
||||
}
|
||||
|
||||
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
|
||||
@@ -198,8 +243,8 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
private func checkAndSetIBColors() {
|
||||
let nonNilColors = [IBColor1, IBColor2, IBColor3].filter { $0 != nil}.map { $0! }
|
||||
if nonNilColors.count > 0 {
|
||||
let nonNilColors = [IBColor1, IBColor2, IBColor3].flatMap { $0 }
|
||||
if !nonNilColors.isEmpty {
|
||||
setColors(nonNilColors)
|
||||
}
|
||||
}
|
||||
@@ -213,15 +258,24 @@ public class KDCircularProgress: UIView {
|
||||
progressLayer.setNeedsDisplay()
|
||||
}
|
||||
|
||||
public func animateFromAngle(fromAngle: Int, toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?) {
|
||||
public func animateFromAngle(fromAngle: Double, toAngle: Double, duration: NSTimeInterval, relativeDuration: Bool = true, completion: ((Bool) -> Void)?) {
|
||||
if isAnimating() {
|
||||
pauseAnimation()
|
||||
}
|
||||
|
||||
|
||||
let animationDuration: NSTimeInterval
|
||||
if relativeDuration {
|
||||
animationDuration = duration
|
||||
} else {
|
||||
let traveledAngle = Utility.mod(toAngle - fromAngle, range: 360, minMax: (0, 360))
|
||||
let scaledDuration = (NSTimeInterval(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
|
||||
@@ -229,15 +283,15 @@ public class KDCircularProgress: UIView {
|
||||
progressLayer.addAnimation(animation, forKey: "angle")
|
||||
}
|
||||
|
||||
public func animateToAngle(toAngle: Int, duration: NSTimeInterval, completion: ((Bool) -> Void)?) {
|
||||
public func animateToAngle(toAngle: Double, duration: NSTimeInterval, relativeDuration: Bool = true, completion: ((Bool) -> Void)?) {
|
||||
if isAnimating() {
|
||||
pauseAnimation()
|
||||
}
|
||||
animateFromAngle(angle, toAngle: toAngle, duration: duration, completion: completion)
|
||||
animateFromAngle(angle, toAngle: toAngle, duration: duration, relativeDuration: relativeDuration, completion: completion)
|
||||
}
|
||||
|
||||
public func pauseAnimation() {
|
||||
let presentationLayer = progressLayer.presentationLayer() as! KDCircularProgressViewLayer
|
||||
guard let presentationLayer = progressLayer.presentationLayer() as? KDCircularProgressViewLayer else { return }
|
||||
let currentValue = presentationLayer.angle
|
||||
progressLayer.removeAllAnimations()
|
||||
animationCompletionBlock = nil
|
||||
@@ -245,7 +299,7 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
|
||||
public func stopAnimation() {
|
||||
let presentationLayer = progressLayer.presentationLayer() as! KDCircularProgressViewLayer
|
||||
animationCompletionBlock = nil
|
||||
progressLayer.removeAllAnimations()
|
||||
angle = 0
|
||||
}
|
||||
@@ -254,10 +308,13 @@ public class KDCircularProgress: UIView {
|
||||
return progressLayer.animationForKey("angle") != nil
|
||||
}
|
||||
|
||||
override public func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
|
||||
override public func animationDidStop(anim: CAAnimation, finished flag: Bool) {
|
||||
if let completionBlock = animationCompletionBlock {
|
||||
if flag {
|
||||
animationCompletionBlock = nil
|
||||
}
|
||||
|
||||
completionBlock(flag)
|
||||
animationCompletionBlock = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,29 +338,44 @@ 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 colorsArray: [UIColor]! {
|
||||
didSet {
|
||||
gradientCache = nil
|
||||
locationsCache = nil
|
||||
invalidateGradientCache()
|
||||
}
|
||||
}
|
||||
var gradientCache: CGGradientRef?
|
||||
var locationsCache: [CGFloat]?
|
||||
private var gradientCache: CGGradientRef?
|
||||
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 glowAmountForAngle(angle: Double, glowAmount: CGFloat, glowMode: KDCircularProgressGlowMode, size: CGFloat) -> CGFloat {
|
||||
switch glowMode {
|
||||
case .Forward:
|
||||
return CGFloat(angle) * size * sizeToGlowRatio * glowAmount
|
||||
@@ -317,11 +389,11 @@ public class KDCircularProgress: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
override class func needsDisplayForKey(key: String!) -> Bool {
|
||||
override class func needsDisplayForKey(key: String) -> Bool {
|
||||
return key == "angle" ? true : super.needsDisplayForKey(key)
|
||||
}
|
||||
|
||||
override init!(layer: AnyObject!) {
|
||||
override init(layer: AnyObject) {
|
||||
super.init(layer: layer)
|
||||
let progressLayer = layer as! KDCircularProgressViewLayer
|
||||
radius = progressLayer.radius
|
||||
@@ -329,6 +401,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
|
||||
@@ -336,126 +409,155 @@ public class KDCircularProgress: UIView {
|
||||
trackThickness = progressLayer.trackThickness
|
||||
trackColor = progressLayer.trackColor
|
||||
colorsArray = progressLayer.colorsArray
|
||||
progressInsideFillColor = progressLayer.progressInsideFillColor
|
||||
}
|
||||
|
||||
override init!() {
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
override func drawInContext(ctx: CGContext!) {
|
||||
override func drawInContext(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)
|
||||
CGContextAddArc(ctx, width/2.0, height/2.0, arcRadius, 0, CGFloat(M_PI * 2), 0)
|
||||
trackColor.set()
|
||||
CGContextSetStrokeColorWithColor(ctx, trackColor.CGColor)
|
||||
CGContextSetFillColorWithColor(ctx, progressInsideFillColor.CGColor)
|
||||
CGContextSetLineWidth(ctx, trackLineWidth)
|
||||
CGContextSetLineCap(ctx, kCGLineCapButt)
|
||||
CGContextDrawPath(ctx, kCGPathStroke)
|
||||
|
||||
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)
|
||||
let reducedAngle = Utility.mod(angle, range: 360, minMax: (0, 360))
|
||||
let fromAngle = Conversion.degreesToRadians(CGFloat(-startAngle))
|
||||
let toAngle = Conversion.degreesToRadians(CGFloat((clockwise == true ? -reducedAngle : reducedAngle) - startAngle))
|
||||
|
||||
CGContextAddArc(imageCtx, width/2.0, height/2.0, arcRadius, fromAngle, toAngle, clockwise == true ? 1 : 0)
|
||||
|
||||
let glowValue = GlowConstants.glowAmountForAngle(reducedAngle, glowAmount: glowAmount, glowMode: glowMode, size: width)
|
||||
if glowValue > 0 {
|
||||
CGContextSetShadowWithColor(imageCtx, CGSizeZero, glowValue, UIColor.blackColor().CGColor)
|
||||
}
|
||||
CGContextSetLineCap(imageCtx, roundedCorners == true ? kCGLineCapRound : kCGLineCapButt)
|
||||
CGContextSetLineCap(imageCtx, roundedCorners == true ? .Round : .Butt)
|
||||
CGContextSetLineWidth(imageCtx, progressLineWidth)
|
||||
CGContextDrawPath(imageCtx, kCGPathStroke)
|
||||
CGContextDrawPath(imageCtx, .Stroke)
|
||||
|
||||
let drawMask: CGImageRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext())
|
||||
let drawMask: CGImageRef = CGBitmapContextCreateImage(UIGraphicsGetCurrentContext())!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
CGContextSaveGState(ctx)
|
||||
CGContextClipToMask(ctx, bounds, 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]
|
||||
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 CGColorGetNumberOfComponents(color.CGColor) == 2 {
|
||||
let whiteValue = CGColorGetComponents(color.CGColor)[0]
|
||||
return UIColor(red: whiteValue, green: whiteValue, blue: whiteValue, alpha: 1.0)
|
||||
} else {
|
||||
return c
|
||||
return color
|
||||
}
|
||||
}
|
||||
|
||||
for color in rgbColorsArray {
|
||||
let colorComponents: UnsafePointer<CGFloat> = CGColorGetComponents(color.CGColor)
|
||||
componentsArray.extend([colorComponents[0],colorComponents[1],colorComponents[2],1.0])
|
||||
let componentsArray = rgbColorsArray.flatMap { color -> [CGFloat] in
|
||||
let components: UnsafePointer<CGFloat> = CGColorGetComponents(color.CGColor)
|
||||
return [components[0], components[1], components[2], 1.0]
|
||||
}
|
||||
|
||||
drawGradientWithContext(ctx, componentsArray: componentsArray)
|
||||
} else {
|
||||
if colorsArray.count == 1 {
|
||||
fillRectWithContext(ctx, color: colorsArray[0])
|
||||
var color: UIColor?
|
||||
if colorsArray.isEmpty {
|
||||
color = UIColor.whiteColor()
|
||||
} 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(t, minMax: ((fi - 1) * step, fi * step))
|
||||
color = Utility.colorLerp(colorT, minMax: (colorsArray[i - 1], colorsArray[i]))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let color = color {
|
||||
fillRectWithContext(ctx, color: color)
|
||||
}
|
||||
}
|
||||
CGContextRestoreGState(ctx)
|
||||
UIGraphicsPopContext()
|
||||
}
|
||||
|
||||
func fillRectWithContext(ctx: CGContext!, color: UIColor) {
|
||||
private func fillRectWithContext(ctx: CGContext!, color: UIColor) {
|
||||
CGContextSetFillColorWithColor(ctx, color.CGColor)
|
||||
CGContextFillRect(ctx, bounds)
|
||||
}
|
||||
|
||||
func drawGradientWithContext(ctx: CGContext!, componentsArray: [CGFloat]) {
|
||||
private func drawGradientWithContext(ctx: CGContext!, componentsArray: [CGFloat]) {
|
||||
let baseSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let locations = locationsCache ?? gradientLocationsFromColorCount(componentsArray.count/4, gradientWidth: bounds.size.width)
|
||||
let locations = locationsCache ?? gradientLocationsForColorCount(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 = CGGradientCreateWithColorComponents(baseSpace, componentsArray, locations,componentsArray.count / 4) else {
|
||||
return
|
||||
}
|
||||
|
||||
gradientCache = cachedGradient
|
||||
gradient = cachedGradient
|
||||
}
|
||||
|
||||
let halfX = bounds.size.width/2.0
|
||||
let halfX = bounds.size.width / 2.0
|
||||
let floatPi = CGFloat(M_PI)
|
||||
let rotateSpeed = clockwise == true ? gradientRotateSpeed : gradientRotateSpeed * -1
|
||||
let angleInRadians = ConversionFunctions.DegreesToRadians(rotateSpeed * CGFloat(angle) - 90)
|
||||
var oppositeAngle = angleInRadians > floatPi ? angleInRadians - floatPi : angleInRadians + floatPi
|
||||
let angleInRadians = Conversion.degreesToRadians(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, 0)
|
||||
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint, .DrawsBeforeStartLocation)
|
||||
}
|
||||
|
||||
func gradientLocationsFromColorCount(colorCount: Int, gradientWidth: CGFloat) -> [CGFloat] {
|
||||
private func gradientLocationsForColorCount(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
BC9E757E1A8CE4A500B1DF3D /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0700;
|
||||
LastUpgradeCheck = 0610;
|
||||
ORGANIZATIONNAME = "Kaan Dedeoglu";
|
||||
TargetAttributes = {
|
||||
@@ -418,6 +419,7 @@
|
||||
BC9E75A71A8CE4A500B1DF3D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
BC9E75A81A8CE4A500B1DF3D /* Build configuration list for PBXNativeTarget "KDCircularProgressExampleTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
@@ -426,6 +428,7 @@
|
||||
BC9E75AA1A8CE4A500B1DF3D /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" 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>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8154"/>
|
||||
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -19,6 +20,7 @@
|
||||
<subviews>
|
||||
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="360" translatesAutoresizingMaskIntoConstraints="NO" id="BHN-x6-Zsx">
|
||||
<rect key="frame" x="61" y="478" width="198" height="31"/>
|
||||
<animations/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="6DH-hv-h4S"/>
|
||||
<constraint firstAttribute="width" constant="194" id="oTI-kD-sOa"/>
|
||||
@@ -29,6 +31,7 @@
|
||||
</slider>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5M2-Yi-RuL">
|
||||
<rect key="frame" x="112" y="523" width="96" height="30"/>
|
||||
<animations/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="96" id="T9z-qf-POZ"/>
|
||||
<constraint firstAttribute="height" constant="30" id="h5a-bV-EMl"/>
|
||||
@@ -42,6 +45,7 @@
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="wev-Tz-pfW" customClass="KDCircularProgress" customModule="KDCircularProgressExample" customModuleProvider="target">
|
||||
<rect key="frame" x="60" y="20" width="200" height="200"/>
|
||||
<animations/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="191-H5-EIc"/>
|
||||
@@ -80,6 +84,7 @@
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
</subviews>
|
||||
<animations/>
|
||||
<color key="backgroundColor" red="0.4736600907821229" green="0.4736600907821229" blue="0.4736600907821229" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="centerX" secondItem="BHN-x6-Zsx" secondAttribute="centerX" id="Arq-5P-YRp"/>
|
||||
@@ -96,9 +101,4 @@
|
||||
<point key="canvasLocation" x="410" y="396"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<simulatedStatusBarMetrics key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
<simulatedScreenMetrics key="destination" type="retina4"/>
|
||||
</simulatedMetricsContainer>
|
||||
</document>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
var progress: KDCircularProgress!
|
||||
|
||||
override func viewDidLoad() {
|
||||
@@ -31,15 +30,15 @@ class ViewController: UIViewController {
|
||||
}
|
||||
|
||||
@IBAction func sliderDidChangeValue(sender: UISlider) {
|
||||
progress.angle = Int(sender.value)
|
||||
progress.angle = Double(sender.value)
|
||||
}
|
||||
|
||||
@IBAction func animateButtonTapped(sender: UIButton) {
|
||||
progress.animateToAngle(360, duration: 5) { completed in
|
||||
progress.animateFromAngle(0, toAngle: 360, duration: 5) { completed in
|
||||
if completed {
|
||||
println("animation stopped, completed")
|
||||
print("animation stopped, completed")
|
||||
} else {
|
||||
println("animation stopped, was interrupted")
|
||||
print("animation stopped, was interrupted")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
# KDCircularProgress
|
||||
`KDCircularProgress` is a circular progress view written in Swift. It makes it possible to have gradients in the progress view, along with glows and animations. Here's an example
|
||||
`KDCircularProgress` is a circular progress view written in Swift. It makes it possible to have gradients in the progress view, along with glows and animations.
|
||||
|
||||
KDCircularProgress also has `IBInspectable` and `IBDesignable` support, so you can configure and preview inside the `Interface Builder`.
|
||||
|
||||
Here's an example
|
||||
|
||||
[Youtube Link](http://youtu.be/iIdas72MXOg)
|
||||
|
||||
|
||||
[](http://youtu.be/iIdas72MXOg)
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
@@ -29,11 +34,14 @@ 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
|
||||
|
||||
####progressColors: `[UIColor]`
|
||||
The colors used to generate the gradient of the progress. You can also set this using the variadic `setColors(UIColor...)` method. A gradient is used only if there is more than one color. A fill is used otherwise. The default is a white fill.
|
||||
|
||||
####angle: `Int`
|
||||
The angle of the progress. Between 0 and 360 (inclusive). Simply change its value in order to change the visual progress of the component. Default is 0.
|
||||
|
||||
@@ -47,7 +55,7 @@ Clockwise if true, Counter-clockwise if false. Default is true.
|
||||
When true, the ends of the progress track will be drawn with a half circle radius. Default is false.
|
||||
|
||||
####gradientRotateSpeed: `CGFloat`
|
||||
Describes how many times the underlying gradient will turn for each full cycle of the progress. Integer values recommended. Default is 0.
|
||||
Describes how many times the underlying gradient will perform a 2π rotation for each full cycle of the progress. Integer values recommended. Default is 0.
|
||||
|
||||
####glowAmount: `CGFloat`
|
||||
The intensity of the glow. Between 0 and 1.0. Default is 1.0.
|
||||
@@ -70,10 +78,10 @@ The thickness of the progress. Between 0 and 1. Default is 0.4
|
||||
The thickness of the background track. Between 0 and 1. Default is 0.5
|
||||
|
||||
####trackColor: `UIColor`
|
||||
The color of the background track. Default is black.
|
||||
The color of the background track. Default is `UIColor.blackColor()`.
|
||||
|
||||
####progressColors: `[UIColor]`
|
||||
The colors used to generate the gradient of the progress. You can also set this using the variadic setColors(UIColor...) method. A gradient is used only if there is more than one color. A fill is used otherwise. The default is a white fill.
|
||||
####progressInsideFillColor: `UIColor`
|
||||
The color of the center of the circle. Default is `UIColor.clearColor()`.
|
||||
|
||||
##Methods
|
||||
```swift
|
||||
@@ -93,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)?)
|
||||
```
|
||||
@@ -122,8 +132,8 @@ Prefering light colors in the gradients gives better results. As mentioned befor
|
||||
##To-Do
|
||||
- [x] Add example project
|
||||
- [ ] Carthage Support
|
||||
- [x] Cocoapods Support
|
||||
- [ ] IBDesignable/IBInspectable 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