diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index a0ef1271..eee7013d 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -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 = ""; }; 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxWithLabelView.swift; sourceTree = ""; }; + 943784F3236B77BB006A1E82 /* GraphView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; + 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphViewAnimationHandler.swift; sourceTree = ""; }; 9455B19B234F8A0400A574DB /* MVMAnimationFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MVMAnimationFramework.framework; path = ../SharedFrameworks/MVMAnimationFramework.framework; sourceTree = ""; }; 948DB67D2326DCD90011F916 /* MultiProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgress.swift; sourceTree = ""; }; D206997521FB8A0B00CAE0DE /* MVMCoreUINavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUINavigationController.h; sourceTree = ""; }; @@ -751,6 +755,8 @@ 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */, 0A7BAFA2232BE63400FB8E22 /* CheckboxWithLabelView.swift */, 01004F2F22721C3800991ECC /* RadioButton.swift */, + 943784F3236B77BB006A1E82 /* GraphView.swift */, + 943784F4236B77BB006A1E82 /* GraphViewAnimationHandler.swift */, ); path = Views; sourceTree = ""; @@ -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 */, diff --git a/MVMCoreUI/Atoms/Views/GraphView.swift b/MVMCoreUI/Atoms/Views/GraphView.swift new file mode 100644 index 00000000..ebd074a0 --- /dev/null +++ b/MVMCoreUI/Atoms/Views/GraphView.swift @@ -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) + } + } +} diff --git a/MVMCoreUI/Atoms/Views/GraphViewAnimationHandler.swift b/MVMCoreUI/Atoms/Views/GraphViewAnimationHandler.swift new file mode 100644 index 00000000..2d7489dc --- /dev/null +++ b/MVMCoreUI/Atoms/Views/GraphViewAnimationHandler.swift @@ -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 + } + +} diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m index e64f643f..8b3f098c 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIMoleculeMappingObject.m @@ -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,