added iOS source code
[wl-app.git] / iOS / Pods / FolioReaderKit / Vendor / HAControls / HADiscreteSlider.swift
1 //
2 //  HADiscreteSlider.swift
3 //  FolioReaderKit
4 //
5 //  Created by Heberti Almeida on 12/02/15.
6 //  Copyright (c) 2015 Folio Reader. All rights reserved.
7 //
8
9 import UIKit
10
11 enum ComponentStyle: Int {
12     case ios
13     case rectangular
14     case rounded
15     case invisible
16     case image
17 }
18
19 let iOSThumbShadowRadius: CGFloat = 4.0
20 let iosThumbShadowOffset = CGSize(width: 0, height: 3)
21
22 class HADiscreteSlider : UIControl {
23
24     func ticksDistanceChanged(_ ticksDistance: CGFloat, sender: AnyObject) { }
25     func valueChanged(_ value: CGFloat) { }
26     
27     // MARK: properties
28     
29     var tickStyle: ComponentStyle =  ComponentStyle.rectangular {
30         didSet { self.layoutTrack() }
31     }
32     
33     var tickSize: CGSize = CGSize(width: 1.0, height: 4.0) {
34         willSet (value) {
35             self.tickSize.width = max(0, value.width)
36             self.tickSize.height = max(0, value.height)
37             self.layoutTrack()
38         }
39     }
40     
41     var tickCount: Int = 11 {
42         willSet (value) {
43             self.tickCount = max(2, value)
44             self.layoutTrack()
45         }
46     }
47     
48     var ticksDistance: CGFloat {
49         get {
50             assert(self.tickCount > 1, "2 ticks minimum \(self.tickCount)")
51             let segments = CGFloat(max(1, self.tickCount-1))
52             return (self.trackRectangle!.size.width/segments)
53         }
54     }
55     
56     var tickImage: String? {
57         didSet { self.layoutTrack() }
58     }
59     
60     var trackStyle: ComponentStyle = ComponentStyle.ios {
61         didSet { self.layoutTrack() }
62     }
63     
64     var trackThickness: CGFloat = 2.0 {
65         willSet (value) {
66             self.trackThickness = max(0, value)
67             self.layoutTrack()
68         }
69     }
70     
71     var trackImage: String? {
72         didSet { self.layoutTrack() }
73     }
74     
75     var thumbStyle: ComponentStyle = ComponentStyle.ios {
76         didSet { self.layoutTrack() }
77     }
78     
79     var thumbSize: CGSize = CGSize(width: 10.0, height: 10.0) {
80         willSet (value) {
81             self.thumbSize.width = max(1, value.width)
82             self.thumbSize.height = max(1, value.height)
83             self.layoutTrack()
84         }
85     }
86     
87     var thumbShadowRadius: CGFloat = 0.0 {
88         didSet { self.layoutTrack() }
89     }
90     
91     var thumbImage: String? {
92         willSet (value) {
93             self.thumbImage = value
94             // Associate image to layer
95             if let imageName = value {
96                 let image: UIImage = UIImage(named: imageName)!
97                 self.thumbLayer!.contents = image.cgImage
98             }
99             self.layoutTrack()
100         }
101     }
102     
103     // AKA: UISlider value (as CGFloat for compatibility with UISlider API, but expected to contain integers)
104     var minimumValue: CGFloat {
105         get { return CGFloat(self._intMinimumValue!) } // calculated property, with a float-to-int adapter
106         set (value) {
107             _intMinimumValue = Int(value);
108             self.layoutTrack()
109         }
110     }
111     
112     var value: CGFloat {
113         get { return CGFloat(self._intValue!) }
114         set (value) {
115             let rootValue = ((value - self.minimumValue) / self.incrementValue)
116             _intValue = Int(self.minimumValue+(rootValue * self.incrementValue))
117             self.layoutTrack()
118         }
119     }
120     
121     var incrementValue: CGFloat = 1 {
122         willSet (value) {
123             self.incrementValue = value
124             if 0 == incrementValue {
125                 self.incrementValue = 1 // nonZeroIncrement
126             }
127             self.layoutTrack()
128         }
129     }
130     
131     var thumbColor: UIColor?
132     var thumbShadowOffset: CGSize?
133     var _intValue: Int?
134         var _intMinimumValue: Int?
135         var ticksAbscisses = [CGPoint]()
136         var thumbAbscisse: CGFloat?
137         var thumbLayer: CALayer?
138         var colorTrackLayer: CALayer?
139         var trackRectangle: CGRect!
140         
141         // When bounds change, recalculate layout
142 //    func setBounds(bounds: CGRect) {
143 //              super.bounds = bounds
144 //              self.layoutTrack()
145 //              self.setNeedsDisplay()
146 //      }
147         
148         override init(frame: CGRect) {
149         super.init(frame: frame)
150         self.initProperties()
151         }
152
153         required init?(coder aDecoder: NSCoder) {
154             fatalError("init(coder:) has not been implemented")
155         }
156         
157         override func draw(_ rect: CGRect) {
158                 self.drawTrack()
159                 self.drawThumb()
160         }
161         
162         func sendActionsForControlEvents() {
163         self.sendActions(for: UIControlEvents.valueChanged)
164         }
165         
166         // MARK: HADiscreteSlider
167     
168         func initProperties() {
169                 self.thumbColor = UIColor.lightGray
170                 self.thumbShadowOffset = CGSize.zero
171                 _intMinimumValue = -5
172                 _intValue = 0
173                 self.thumbAbscisse = 0.0
174                 self.trackRectangle = CGRect.zero
175                 // In case we need a colored track, initialize it now
176                 // There may be a more elegant way to do this than with a CALayer,
177                 // but then again CALayer brings free animation and will animate along the thumb
178                 self.colorTrackLayer = CALayer()
179                 self.colorTrackLayer!.backgroundColor = UIColor(hue: 211.0/360.0, saturation: 1, brightness: 1, alpha: 1).cgColor
180                 self.colorTrackLayer!.cornerRadius = 2.0
181                 self.layer.addSublayer(self.colorTrackLayer!)
182                 // The thumb is its own CALayer, which brings in free animation
183                 self.thumbLayer = CALayer()
184                 self.layer.addSublayer(self.thumbLayer!)
185                 self.isMultipleTouchEnabled = false
186                 self.layoutTrack()
187         }
188         
189         func drawTrack() {
190                 let ctx = UIGraphicsGetCurrentContext()
191                 // Track
192                 switch self.trackStyle {
193         case .rectangular:
194             ctx!.addRect(self.trackRectangle)
195         break
196         case .image:
197         
198             // Draw image if exists
199             if let imageName = self.trackImage {
200                 let image = UIImage(named:imageName)!
201                 let centered = CGRect(x: (self.frame.size.width/2)-(image.size.width/2), y: (self.frame.size.height/2)-(image.size.height/2), width: image.size.width, height: image.size.height)
202                     ctx!.draw(image.cgImage!, in: centered)
203             }
204             break
205         
206         case .invisible, .rounded, .ios:
207             let path: UIBezierPath = UIBezierPath(roundedRect: self.trackRectangle, cornerRadius: self.trackRectangle.size.height/2)
208             ctx!.addPath(path.cgPath)
209             break
210                 }
211         
212                 // Ticks
213                 if .ios != self.tickStyle {
214             for originValue in self.ticksAbscisses {
215                 let originPoint = originValue
216                 let rectangle = CGRect(x: originPoint.x-(self.tickSize.width/2), y: originPoint.y-(self.tickSize.height/2), width: self.tickSize.width, height: self.tickSize.height)
217                 switch self.tickStyle {
218                 case .rounded:
219                     let path = UIBezierPath(roundedRect: rectangle, cornerRadius: rectangle.size.height/2)
220                     ctx!.addPath(path.cgPath)
221                     break
222                 case .rectangular:
223                     ctx!.addRect(rectangle)
224                     break
225                 case .image:
226                     // Draw image if exists
227                     
228                     if let imageName = self.tickImage {
229                         let image = UIImage(named: imageName)!
230                         let centered = CGRect(x: rectangle.origin.x+(rectangle.size.width/2)-(image.size.width/2), y: rectangle.origin.y+(rectangle.size.height/2)-(image.size.height/2), width: image.size.width, height: image.size.height)
231                             ctx!.draw(image.cgImage!, in: centered)
232                     }
233                     break
234                 
235                 case .invisible: break
236                 case .ios: break
237                 }
238             }
239                 }
240         
241                 // iOS UISlider aka .IOS does not have ticks
242                 ctx!.setFillColor(self.tintColor.cgColor)
243                 ctx!.fillPath()
244                 // For colored track, we overlay a CALayer, which will animate along with the cursor
245                 if .ios == self.trackStyle {
246                         var frame = self.trackRectangle
247                         frame?.size.width = self.thumbAbscisse!-self.trackRectangle.minX
248                         self.colorTrackLayer!.frame = frame!
249                 } else {
250                         self.colorTrackLayer!.frame = CGRect.zero
251                 }
252         }
253         
254         func drawThumb() {
255                 if self.value >= self.minimumValue {
256                         // Feature: hide the thumb when below range
257                         let thumbSizeForStyle = self.thumbSizeIncludingShadow()
258                         let thumbWidth = thumbSizeForStyle.width
259                         let thumbHeight = thumbSizeForStyle.height
260                         let rectangle = CGRect(x: self.thumbAbscisse!-(thumbWidth/2), y: (self.frame.size.height-thumbHeight)/2, width: thumbWidth, height: thumbHeight)
261                         let shadowRadius = ((self.thumbStyle == .ios) ? iOSThumbShadowRadius : self.thumbShadowRadius)
262                         let shadowOffset = ((self.thumbStyle == .ios) ? iosThumbShadowOffset : self.thumbShadowOffset)
263                         // Ignore offset if there is no shadow
264                         self.thumbLayer!.frame = ((shadowRadius != 0.0) ? rectangle.insetBy(dx: shadowRadius+shadowOffset!.width, dy: shadowRadius+shadowOffset!.height) : rectangle.insetBy(dx: shadowRadius, dy: shadowRadius))
265                         switch self.thumbStyle {
266             case .rounded:
267                 // A rounded thumb is circular
268                 self.thumbLayer!.backgroundColor = self.thumbColor!.cgColor
269                 self.thumbLayer!.borderColor = UIColor.clear.cgColor
270                 self.thumbLayer!.borderWidth = 0.0
271                 self.thumbLayer!.cornerRadius = self.thumbLayer!.frame.size.width/2
272                 self.thumbLayer!.allowsEdgeAntialiasing = true
273                 break
274                                 
275             case .image:
276                 // image is set using layer.contents
277                 self.thumbLayer!.backgroundColor = UIColor.clear.cgColor
278                 self.thumbLayer!.borderColor = UIColor.clear.cgColor
279                 self.thumbLayer!.borderWidth = 0.0
280                 self.thumbLayer!.cornerRadius = 0.0
281                 self.thumbLayer!.allowsEdgeAntialiasing = false
282                 break
283                                 
284             case .rectangular:
285                 self.thumbLayer!.backgroundColor = self.thumbColor!.cgColor
286                                 self.thumbLayer!.borderColor = UIColor.clear.cgColor
287                                 self.thumbLayer!.borderWidth = 0.0
288                                 self.thumbLayer!.cornerRadius = 0.0
289                                 self.thumbLayer!.allowsEdgeAntialiasing = false
290                                 break
291                 
292             case .invisible:
293                                 self.thumbLayer!.backgroundColor = UIColor.clear.cgColor
294                                 self.thumbLayer!.cornerRadius = 0.0
295                                 break
296                 
297             case .ios:
298                 self.thumbLayer!.backgroundColor = UIColor.white.cgColor
299                 self.thumbLayer!.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.8, alpha: 1).cgColor
300                 self.thumbLayer!.borderWidth = 0.5
301                 self.thumbLayer!.cornerRadius = self.thumbLayer!.frame.size.width/2
302                 self.thumbLayer!.allowsEdgeAntialiasing = true
303                 break
304                         }
305             
306             
307                         // Shadow
308                         if shadowRadius != 0.0 {
309                                 self.thumbLayer!.shadowOffset = shadowOffset!
310                                 self.thumbLayer!.shadowRadius = shadowRadius
311                                 self.thumbLayer!.shadowColor = UIColor.black.cgColor
312                                 self.thumbLayer!.shadowOpacity = 0.15
313                         } else {
314                                 self.thumbLayer!.shadowRadius = 0.0
315                                 self.thumbLayer!.shadowOffset = CGSize.zero
316                                 self.thumbLayer!.shadowColor = UIColor.clear.cgColor
317                                 self.thumbLayer!.shadowOpacity = 0.0
318                         }
319                 }
320         }
321         
322         func layoutTrack() {
323                 assert(self.tickCount > 1, "2 ticks minimum \(self.tickCount)")
324                 let segments = max(1, self.tickCount-1)
325                 let thumbWidth = self.thumbSizeIncludingShadow().width
326                 
327         // Calculate the track ticks positions
328                 let trackHeight = ((.ios == self.trackStyle) ? 2.0 : self.trackThickness)
329                 var trackSize = CGSize(width: self.frame.size.width-thumbWidth, height: trackHeight)
330                 if .image == self.trackStyle {
331                         if let imageName = self.trackImage {
332                                 let image = UIImage(named: imageName)!
333                 trackSize.width = image.size.width-thumbWidth
334                         }
335                 }
336                 self.trackRectangle = CGRect(x: (self.frame.size.width-trackSize.width)/2, y: (self.frame.size.height-trackSize.height)/2, width: trackSize.width, height: trackSize.height)
337                 let trackY = self.frame.size.height/2
338                 
339         self.ticksAbscisses.removeAll()
340                 
341         for i in 0...segments {
342             let ratio = Double(i) / Double(segments)
343             let originX = self.trackRectangle.origin.x+(trackSize.width * CGFloat(ratio))
344             let point = CGPoint(x:originX, y:trackY)
345             self.ticksAbscisses.append(point)
346         }
347         
348                 self.layoutThumb()
349         }
350         
351         func layoutThumb() {
352                 assert(self.tickCount > 1, "2 ticks minimum \(self.tickCount)")
353                 let segments = max(1, self.tickCount-1)
354                 // Calculate the thumb position
355                 var thumbRatio = (self.value-self.minimumValue) / CGFloat(segments) * self.incrementValue
356                 thumbRatio = max(0.0, min(thumbRatio, 1.0))
357                 // Normalized
358                 self.thumbAbscisse = self.trackRectangle.origin.x+(self.trackRectangle.size.width*thumbRatio)
359         }
360         
361         func thumbSizeIncludingShadow() -> CGSize {
362                 switch self.thumbStyle {
363         case .invisible: break
364         case .rectangular: break
365         case .rounded:
366             return ((self.thumbShadowRadius != 0.0) ? CGSize(width: self.thumbSize.width+(self.thumbShadowRadius*2)+(self.thumbShadowOffset!.width*2), height: self.thumbSize.height+(self.thumbShadowRadius*2)+(self.thumbShadowOffset!.height*2)) : self.thumbSize)
367         case .ios:
368             return CGSize(width: 33.0+(iOSThumbShadowRadius*2)+(iosThumbShadowOffset.width*2), height: 33.0+(iOSThumbShadowRadius*2)+(iosThumbShadowOffset.height*2))
369         case .image:
370             if let imageName = self.thumbImage {
371                 return UIImage(named: imageName)!.size
372             }
373                 }
374         return CGSize(width: 33.0, height: 33.0)
375         }
376         
377         // MARK: Touches
378     
379     override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
380                 self.touchDown(touches, duration: 0.1)
381         }
382         
383     override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
384                 self.touchDown(touches, duration: 0.0)
385         }
386         
387     override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
388         self.touchUp(touches)
389         }
390         
391     override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
392         self.touchUp(touches)
393         }
394         
395         func touchDown(_ touches: Set<UITouch>, duration: TimeInterval) {
396         guard let touch = touches.first else { return }
397         let location = touch.location(in: touch.view)
398         self.moveThumbTo(location.x, duration: duration)
399         }
400         
401         func touchUp(_ touches: Set<UITouch>) {
402         guard let touch = touches.first else { return }
403         let location = touch.location(in: touch.view)
404         let tick = self.pickTickFromSliderPosition(location.x)
405         self.moveThumbToTick(tick)
406         }
407         
408         // MARK: Notifications
409     
410         func moveThumbToTick(_ tick: Int) {
411                 let intValue = Int(self.minimumValue)+(tick * Int(self.incrementValue))
412                 if intValue != _intValue {
413                         _intValue = intValue
414                         self.sendActionsForControlEvents()
415                 }
416                 self.layoutThumb()
417                 self.setNeedsDisplay()
418         }
419         
420         func moveThumbTo(_ abscisse: CGFloat, duration: CFTimeInterval) {
421                 let leftMost = self.trackRectangle.minX
422                 let rightMost = self.trackRectangle.maxX
423                 self.thumbAbscisse = max(leftMost, min(abscisse, rightMost))
424                 CATransaction.setAnimationDuration(duration)
425                 let tick = self.pickTickFromSliderPosition(self.thumbAbscisse!)
426                 let intValue = Int(self.minimumValue)+(tick * Int(self.incrementValue))
427                 if intValue != _intValue {
428                         _intValue = intValue
429                         self.sendActionsForControlEvents()
430                 }
431                 self.setNeedsDisplay()
432         }
433         
434         func pickTickFromSliderPosition(_ abscisse: CGFloat) -> Int {
435                 let leftMost = self.trackRectangle.minX
436                 let rightMost = self.trackRectangle.maxX
437                 let clampedAbscisse = max(leftMost, min(abscisse, rightMost))
438                 let ratio = Double(clampedAbscisse-leftMost) / Double(rightMost-leftMost)
439                 let segments = Double(max(1, self.tickCount-1))
440                 return Int(round(segments*ratio))
441         }
442         
443 }
444