create circle progress view
This commit is contained in:
parent
80c277f4e8
commit
7870a116c6
@ -28,6 +28,8 @@
|
||||
0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A41BA6D2344FCD400D4C0BC /* CATransaction+Extension.swift */; };
|
||||
0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */; };
|
||||
0A7BAFA1232BE61800FB8E22 /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */; };
|
||||
943784F5236B77BB006A1E82 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F3236B77BB006A1E82 /* GraphView.swift */; };
|
||||
943784F6236B77BB006A1E82 /* GraphViewAnimationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */; };
|
||||
9455B19C234F8A0400A574DB /* MVMAnimationFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */; };
|
||||
948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948DB67D2326DCD90011F916 /* MultiProgress.swift */; };
|
||||
D206997721FB8A0B00CAE0DE /* MVMCoreUINavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
@ -223,6 +225,8 @@
|
||||
0A7BAD73232A8DC700FB8E22 /* HeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyButton.swift; sourceTree = "<group>"; };
|
||||
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = "<group>"; };
|
||||
0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxWithLabelView.swift; sourceTree = "<group>"; };
|
||||
943784F3236B77BB006A1E82 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = "<group>"; };
|
||||
943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphViewAnimationHandler.swift; sourceTree = "<group>"; };
|
||||
9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MVMAnimationFramework.framework; path = ../SharedFrameworks/MVMAnimationFramework.framework; sourceTree = "<group>"; };
|
||||
948DB67D2326DCD90011F916 /* MultiProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgress.swift; sourceTree = "<group>"; };
|
||||
D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUINavigationController.h; sourceTree = "<group>"; };
|
||||
@ -751,6 +755,8 @@
|
||||
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */,
|
||||
0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */,
|
||||
01004F2F22721C3800991ECC /* RadioButton.swift */,
|
||||
943784F3236B77BB006A1E82 /* GraphView.swift */,
|
||||
943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -1038,6 +1044,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
943784F5236B77BB006A1E82 /* GraphView.swift in Sources */,
|
||||
D29DF32121ED0CBA003B2FB9 /* LabelView.m in Sources */,
|
||||
DBC4391822442197001AB423 /* CaretView.swift in Sources */,
|
||||
D29770F221F7C6D600B2F0D0 /* TopLabelsAndBottomButtonsTableViewController.m in Sources */,
|
||||
@ -1124,6 +1131,7 @@
|
||||
D2E1FADF2268B8E700AEFD8C /* ThreeLayerTableViewController.swift in Sources */,
|
||||
D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */,
|
||||
D2B1E3E522F37D6A0065F95C /* ImageHeadlineBody.swift in Sources */,
|
||||
943784F6236B77BB006A1E82 /* GraphViewAnimationHandler.swift in Sources */,
|
||||
D29DF2AA21E7B2F9003B2FB9 /* MVMCoreUIConstants.m in Sources */,
|
||||
948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */,
|
||||
D2A5146122121FBF00345BFB /* MoleculeStackTemplate.swift in Sources */,
|
||||
|
||||
280
MVMCoreUI/Atoms/Views/GraphView.swift
Normal file
280
MVMCoreUI/Atoms/Views/GraphView.swift
Normal file
@ -0,0 +1,280 @@
|
||||
//
|
||||
// GraphView.swift
|
||||
// MobileFirstFramework
|
||||
//
|
||||
// Created by Ryan on 10/24/19.
|
||||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
enum GraphSize: String {
|
||||
case small, medium, large
|
||||
}
|
||||
|
||||
enum GraphStyle: String {
|
||||
case unlimited, safetyMode
|
||||
}
|
||||
|
||||
///Graph Object contains properties
|
||||
public struct GraphObject {
|
||||
|
||||
var style: GraphStyle {
|
||||
didSet {
|
||||
updateStyle()
|
||||
}
|
||||
}
|
||||
var size: GraphSize {
|
||||
didSet {
|
||||
updateSize()
|
||||
}
|
||||
}
|
||||
var diameter: CGFloat = 24
|
||||
var lineWidth: CGFloat = 5
|
||||
var clockwise: Bool = true
|
||||
var duration : Double = 1.0
|
||||
var colors = [CGColor]()
|
||||
|
||||
public init(_ json: [AnyHashable : Any]?) {
|
||||
style = .unlimited
|
||||
size = .small
|
||||
guard let json = json else {
|
||||
return
|
||||
}
|
||||
if let styleString = json.optionalStringForKey("style") {
|
||||
style = GraphStyle(rawValue: styleString) ?? .unlimited
|
||||
}
|
||||
if let sizeString = json.optionalStringForKey("size") {
|
||||
size = GraphSize(rawValue: sizeString) ?? .small
|
||||
}
|
||||
updateStyle()
|
||||
updateSize()
|
||||
if let diameter = json.optionalCGFloatForKey("diameter") {
|
||||
self.diameter = diameter
|
||||
}
|
||||
if let lineWidth = json.optionalCGFloatForKey("lineWidth") {
|
||||
self.lineWidth = lineWidth
|
||||
}
|
||||
if let clockwise = json.optionalBoolForKey("clockwise") {
|
||||
self.clockwise = clockwise
|
||||
}
|
||||
if let duration = json["duration"] as? Double {
|
||||
self.duration = duration
|
||||
}
|
||||
if let colorArray = json.optionalArrayForKey("colors") as? [String] {
|
||||
colors = getCGColorsFromArray(colorArray)
|
||||
}
|
||||
}
|
||||
|
||||
func getCGColorsFromArray(_ colorArray: [String]) -> [CGColor] {
|
||||
return colorArray.map { (colorString) -> CGColor in
|
||||
return UIColor.mfGet(forHex: colorString).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
mutating func updateStyle() {
|
||||
switch style {
|
||||
case .unlimited:
|
||||
duration = 1.0
|
||||
clockwise = true
|
||||
//current style, only the end part shows darker look
|
||||
colors = getCGColorsFromArray(["#007AB8","#007AB8","#033554"])
|
||||
break
|
||||
case .safetyMode:
|
||||
duration = 1.5
|
||||
clockwise = true
|
||||
colors = getCGColorsFromArray(["#CC4D0F","#CC4D0F","AB0309"])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//those are
|
||||
mutating func updateSize() {
|
||||
switch size {
|
||||
case .small:
|
||||
diameter = MFSizeObject(standardSize: 24)?.getValueBasedOnApplicationWidth() ?? 24
|
||||
lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5
|
||||
break
|
||||
case .medium:
|
||||
diameter = MFSizeObject(standardSize: 100)?.getValueBasedOnApplicationWidth() ?? 100
|
||||
lineWidth = MFSizeObject(standardSize: 8)?.getValueBasedOnApplicationWidth() ?? 8
|
||||
break
|
||||
case .large:
|
||||
diameter = MFSizeObject(standardSize: 180)?.getValueBasedOnApplicationWidth() ?? 180
|
||||
lineWidth = MFSizeObject(standardSize: 12)?.getValueBasedOnApplicationWidth() ?? 12
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objcMembers open class GraphView: View {
|
||||
|
||||
var heightConstraint: NSLayoutConstraint?
|
||||
var gradientLayer: CALayer?
|
||||
var graphObject: GraphObject?
|
||||
|
||||
|
||||
// MARK: setup
|
||||
open override func setupView() {
|
||||
super.setupView()
|
||||
//avoid adding height constraint multiple times
|
||||
guard heightConstraint == nil else { return }
|
||||
heightConstraint = heightAnchor.constraint(equalToConstant: 0)
|
||||
heightConstraint?.isActive = true
|
||||
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
|
||||
}
|
||||
|
||||
override open func setWithJSON(_ json: [AnyHashable : Any]?, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable : Any]?) {
|
||||
super.setWithJSON(json, delegateObject: delegateObject, additionalData: additionalData)
|
||||
let object = GraphObject(json)
|
||||
graphObject = object
|
||||
createGraphCircle(object)
|
||||
rotationAnimation(object)
|
||||
}
|
||||
|
||||
class func getAngle(_ piValue: Double) -> Double {
|
||||
return piValue / (2.0 * Double.pi) * 360.0
|
||||
}
|
||||
|
||||
class func getPiValue(_ angle: Double) -> Double {
|
||||
return angle / 360.0 * 2.0 * Double.pi
|
||||
}
|
||||
|
||||
// MARK: circle
|
||||
open func createGraphCircle(_ graphObject: GraphObject) {
|
||||
if let sublayers = layer.sublayers {
|
||||
for sublayer in sublayers {
|
||||
sublayer.removeAllAnimations()
|
||||
sublayer.removeFromSuperlayer()
|
||||
}
|
||||
}
|
||||
heightConstraint?.constant = graphObject.diameter
|
||||
|
||||
//create circle path
|
||||
let radius = graphObject.diameter / 2.0
|
||||
|
||||
//begin point will be at the bottom, clockwise direction
|
||||
let path = UIBezierPath(arcCenter: CGPoint(x: radius
|
||||
, y: radius), radius: radius - graphObject.lineWidth/2.0, startAngle: CGFloat(GraphView.getPiValue(90.0)), endAngle: CGFloat(GraphView.getPiValue(90.0 + 360.0)), clockwise: true)
|
||||
path.lineWidth = graphObject.lineWidth
|
||||
|
||||
let circleLayer = CAShapeLayer()
|
||||
circleLayer.path = path.cgPath
|
||||
circleLayer.lineCap = .round
|
||||
circleLayer.lineWidth = graphObject.lineWidth
|
||||
circleLayer.fillColor = UIColor.clear.cgColor
|
||||
circleLayer.strokeColor = UIColor.black.cgColor
|
||||
|
||||
//create gradient layer
|
||||
let gradientLayer = createGradientLayer(graphObject)
|
||||
gradientLayer.mask = circleLayer
|
||||
layer.addSublayer(gradientLayer)
|
||||
self.gradientLayer = gradientLayer
|
||||
}
|
||||
|
||||
/*
|
||||
create three gradient layer for circle layout.
|
||||
_____________
|
||||
| → | top layer for smooth gradient
|
||||
-------------
|
||||
| | |
|
||||
| ↑ | ↓ |
|
||||
| | |
|
||||
-------------
|
||||
*/
|
||||
func createGradientLayer(_ graphObject: GraphObject) -> CALayer {
|
||||
let containLayer = CALayer()
|
||||
containLayer.frame = CGRect(x: 0, y: 0, width: graphObject.diameter, height: graphObject.diameter)
|
||||
let radius = graphObject.diameter / 2.0
|
||||
|
||||
//create graident layers
|
||||
guard graphObject.colors.count > 1 else {
|
||||
containLayer.backgroundColor = graphObject.colors.first
|
||||
return containLayer
|
||||
}
|
||||
var topGradientHeight : CGFloat = 0.0
|
||||
var leftColors = graphObject.colors.prefix(through: graphObject.colors.count/2)
|
||||
let rightColors = graphObject.colors.suffix(from: graphObject.colors.count/2)
|
||||
|
||||
// make the top layer higher than line width for smooth look
|
||||
topGradientHeight = min(max(graphObject.lineWidth, 1.0/(1.0+CGFloat(graphObject.colors.count))*graphObject.diameter), graphObject.diameter)
|
||||
let topLayer = CAGradientLayer()
|
||||
topLayer.frame = CGRect(x: 0.0, y: 0.0, width: graphObject.diameter, height: topGradientHeight)
|
||||
//make the graident edge more smoothy
|
||||
topLayer.startPoint = CGPoint(x: 0.25, y: 0.0)
|
||||
topLayer.endPoint = CGPoint(x: 0.75, y: 0.0)
|
||||
//if number of colors is even, need to display gradient layer, otherwise make top layer as solid color layer
|
||||
if graphObject.colors.count % 2 == 0 {
|
||||
leftColors.removeLast()
|
||||
topLayer.colors = [leftColors.last!, rightColors.first!]
|
||||
} else {
|
||||
topLayer.backgroundColor = leftColors.last
|
||||
}
|
||||
containLayer.addSublayer(topLayer)
|
||||
|
||||
let leftLayer = CAGradientLayer()
|
||||
leftLayer.frame = CGRect(x: 0, y: topGradientHeight, width: radius, height: graphObject.diameter - topGradientHeight)
|
||||
leftLayer.startPoint = CGPoint(x: 0, y: 1)
|
||||
leftLayer.endPoint = CGPoint(x: 0, y: 0)
|
||||
|
||||
//count of graidentLayer.colors must be bigger than 1, otherwise set backgroundColor
|
||||
if leftColors.count > 1 {
|
||||
leftLayer.colors = Array(leftColors)
|
||||
} else {
|
||||
leftLayer.backgroundColor = leftColors.first
|
||||
}
|
||||
containLayer.addSublayer(leftLayer)
|
||||
|
||||
let rightLayer = CAGradientLayer()
|
||||
rightLayer.frame = CGRect(x: radius, y: topGradientHeight, width: radius, height: graphObject.diameter - topGradientHeight)
|
||||
rightLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||
rightLayer.endPoint = CGPoint(x: 0, y: 1)
|
||||
if rightColors.count > 1 {
|
||||
rightLayer.colors = Array(rightColors)
|
||||
} else {
|
||||
rightLayer.backgroundColor = rightColors.first
|
||||
}
|
||||
containLayer.addSublayer(rightLayer)
|
||||
|
||||
return containLayer
|
||||
}
|
||||
|
||||
//MARK: Animation
|
||||
func rotationAnimation(_ object: GraphObject) {
|
||||
MVMCoreDispatchUtility.performBlock(onMainThread:{
|
||||
let rotation = CABasicAnimation(keyPath: "transform.rotation")
|
||||
let animationHandler = GraphViewAnimationHandler.shared
|
||||
let startAngle = animationHandler.getAnimationStartAngle(object.duration, CACurrentMediaTime())
|
||||
if startAngle == 0.0 {
|
||||
animationHandler.storeAnimation(object.duration, CACurrentMediaTime())
|
||||
}
|
||||
var fromValue = GraphView.getPiValue(0.0 + startAngle), toValue = GraphView.getPiValue(360.0 + startAngle)
|
||||
if !object.clockwise {
|
||||
fromValue = GraphView.getPiValue(360.0 - startAngle)
|
||||
toValue = GraphView.getPiValue(0.0 - startAngle)
|
||||
}
|
||||
rotation.fromValue = fromValue
|
||||
rotation.toValue = toValue
|
||||
rotation.duration = object.duration
|
||||
rotation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
rotation.fillMode = .both
|
||||
rotation.isRemovedOnCompletion = false
|
||||
|
||||
//avoid infinity animation take high CPU momery usage when layer is not displayed
|
||||
rotation.delegate = self
|
||||
rotation.repeatCount = 1
|
||||
self.gradientLayer?.add(rotation, forKey: "rotation")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension GraphView: CAAnimationDelegate {
|
||||
public func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
|
||||
if let object = graphObject {
|
||||
rotationAnimation(object)
|
||||
}
|
||||
}
|
||||
}
|
||||
32
MVMCoreUI/Atoms/Views/GraphViewAnimationHandler.swift
Normal file
32
MVMCoreUI/Atoms/Views/GraphViewAnimationHandler.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// GraphViewAnimationHandler.swift
|
||||
// MobileFirstFramework
|
||||
//
|
||||
// Created by Ryan on 10/29/19.
|
||||
// Copyright © 2019 Verizon Wireless. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objcMembers open class GraphViewAnimationHandler: NSObject {
|
||||
|
||||
/// duration : CACurrentMediaTime()
|
||||
private var animations = [Double: Double]()
|
||||
|
||||
static let shared = GraphViewAnimationHandler()
|
||||
|
||||
open func storeAnimation(_ duration: Double, _ currentTime: CFTimeInterval) {
|
||||
guard animations[duration] == nil else {
|
||||
return
|
||||
}
|
||||
animations[duration] = currentTime
|
||||
}
|
||||
|
||||
open func getAnimationStartAngle(_ duration: Double, _ currentTime: CFTimeInterval) -> Double {
|
||||
if let time = animations[duration] {
|
||||
return (currentTime - time) / duration * 360 + 90
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
}
|
||||
@ -42,6 +42,7 @@
|
||||
@"checkboxWithLabelView" : CheckboxWithLabelView.class,
|
||||
@"cornerLabels" : CornerLabels.class,
|
||||
@"progressbar": ProgressBar.class,
|
||||
@"circleProgress": GraphView.class,
|
||||
@"multiProgressBar": MultiProgress.class,
|
||||
@"checkbox": MVMCoreUICheckBox.class,
|
||||
@"radioButton": RadioButton.class,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user