Add support for circular progress bar part 2

This commit is contained in:
Xi Zhang 2024-07-09 18:49:10 -04:00
parent 7fec6f540e
commit 1b0197ed2c
3 changed files with 90 additions and 43 deletions

View File

@ -11,11 +11,82 @@ import UIKit
@objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol { @objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol {
var heightConstraint: NSLayoutConstraint? var heightConstraint: NSLayoutConstraint?
weak var gradientLayer: CALayer?
var graphModel: CircularProgressBarModel? { var graphModel: CircularProgressBarModel? {
return model as? CircularProgressBarModel return model as? CircularProgressBarModel
} }
private var progressLayer = CAShapeLayer()
private var tracklayer = CAShapeLayer()
private var labelLayer = CATextLayer()
var setProgressColor: UIColor = UIColor.red {
didSet {
progressLayer.strokeColor = setProgressColor.cgColor
}
}
var setTrackColor: UIColor = UIColor.white {
didSet {
tracklayer.strokeColor = setTrackColor.cgColor
}
}
/**
A path that consists of straight and curved line segments that you can render in your custom views.
Meaning our CAShapeLayer will now be drawn on the screen with the path we have specified here
*/
private var viewCGPath: CGPath? {
let width = graphModel?.diameter ?? 84
let height = width
return UIBezierPath(arcCenter: CGPoint(x: width / 2.0, y: height / 2.0),
radius: (width - 1.5)/2,
startAngle: CGFloat(-0.5 * Double.pi),
endAngle: CGFloat(1.5 * Double.pi), clockwise: true).cgPath
}
private func configureProgressViewToBeCircular() {
let lineWidth = graphModel?.lineWidth ?? 2.0
self.backgroundColor = UIColor.clear
self.drawShape(using: tracklayer, lineWidth: lineWidth)
self.drawShape(using: progressLayer, lineWidth: lineWidth)
}
private func drawShape(using shape: CAShapeLayer, lineWidth: CGFloat) {
shape.path = self.viewCGPath
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = lineWidth
self.layer.addSublayer(shape)
}
func setProgressWithAnimation(duration: TimeInterval, value: Float) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = 0 //start animation at point 0
animation.toValue = value //end animation at point specified
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
progressLayer.strokeEnd = CGFloat(value)
progressLayer.add(animation, forKey: "animateCircle")
}
func drawLabel() {
let percent = graphModel?.percent ?? 0
let percentLen = percent > 9 ? 2 : 1
let attributedString = NSMutableAttributedString(string: String(percent) + "%")
attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldTitleLarge()], range: NSMakeRange(0, percentLen))
attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldBodyLarge()], range: NSMakeRange(percentLen, 1))
// Text layer
let width = graphModel?.diameter ?? 84
let height = width
labelLayer.string = attributedString
labelLayer.frame = CGRectMake((width - CGFloat(percentLen * 20))/2, (height - 30)/2, 60, 30)
self.layer.addSublayer(labelLayer)
}
// MARK: setup // MARK: setup
open override func setupView() { open override func setupView() {
super.setupView() super.setupView()
@ -27,46 +98,21 @@ import UIKit
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData) super.set(with: model, delegateObject, additionalData)
guard let model = model as? CircularProgressBarModel else { return } guard let model = model as? CircularProgressBarModel else { return }
createGraphCircle(model)
} configureProgressViewToBeCircular()
class func getAngle(_ piValue: Double) -> Double { if let color = model.color {
return piValue / (2.0 * Double.pi) * 360.0 setProgressColor = color.uiColor
}
class func getPiValue(_ angle: Double) -> Double {
return angle / 360.0 * 2.0 * Double.pi
}
// MARK: circle
open func createGraphCircle(_ graphObject: CircularProgressBarModel) {
if let sublayers = layer.sublayers {
for sublayer in sublayers {
sublayer.removeAllAnimations()
sublayer.removeFromSuperlayer()
}
} }
heightConstraint?.constant = graphObject.diameter
let gradient = CAGradientLayer() if let backgroundColor = model.backgroundColor {
gradient.type = .conic setTrackColor = backgroundColor.uiColor
gradient.startPoint = CGPoint(x: 0.5, y: 0.5) }
gradient.endPoint = CGPoint(x: 0.5, y: 0.0)
gradient.frame = CGRect(x: 0, y: 0, width: graphObject.diameter, height: graphObject.diameter)
gradientLayer = gradient
layer.addSublayer(gradient)
let center = CGPoint(x: gradient.bounds.midX, y: gradient.bounds.midY) setProgressWithAnimation(duration: 0.5, value: Float(graphModel?.percent ?? 0) / 100)
let radius = (graphObject.diameter - graphObject.lineWidth) / 2.0 drawLabel()
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: (3 / 2 * .pi), endAngle: -(1 / 2 * .pi), clockwise: false)
let mask = CAShapeLayer()
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.white.cgColor
mask.lineWidth = graphObject.lineWidth
mask.path = path.cgPath
gradient.mask = mask
} }
//MARK: MVMCoreUIViewConstrainingProtocol //MARK: MVMCoreUIViewConstrainingProtocol
public func needsToBeConstrained() -> Bool { public func needsToBeConstrained() -> Bool {
return true return true

View File

@ -83,16 +83,16 @@ public class CircularProgressBarModel: MoleculeModelProtocol {
func updateSize() { func updateSize() {
switch size { switch size {
case .small: case .small:
diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20 diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4 lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5
break break
case .medium: case .medium:
diameter = MFSizeObject(standardSize: 100)?.getValueBasedOnApplicationWidth() ?? 100 diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84
lineWidth = MFSizeObject(standardSize: 8)?.getValueBasedOnApplicationWidth() ?? 8 lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5
break break
case .large: case .large:
diameter = MFSizeObject(standardSize: 180)?.getValueBasedOnApplicationWidth() ?? 180 diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124
lineWidth = MFSizeObject(standardSize: 12)?.getValueBasedOnApplicationWidth() ?? 12 lineWidth = MFSizeObject(standardSize: 5)?.getValueBasedOnApplicationWidth() ?? 5
break break
} }
} }

View File

@ -67,6 +67,7 @@ open class CoreUIModelMapping: ModelMapping {
ModelRegistry.register(handler: LoadImageView.self, for: ImageViewModel.self) ModelRegistry.register(handler: LoadImageView.self, for: ImageViewModel.self)
ModelRegistry.register(handler: Line.self, for: LineModel.self) ModelRegistry.register(handler: Line.self, for: LineModel.self)
ModelRegistry.register(handler: Wheel.self, for: WheelModel.self) ModelRegistry.register(handler: Wheel.self, for: WheelModel.self)
ModelRegistry.register(handler: CircularProgressBar.self, for: CircularProgressBarModel.self)
ModelRegistry.register(handler: Toggle.self, for: ToggleModel.self) ModelRegistry.register(handler: Toggle.self, for: ToggleModel.self)
ModelRegistry.register(handler: CheckboxLabel.self, for: CheckboxLabelModel.self) ModelRegistry.register(handler: CheckboxLabel.self, for: CheckboxLabelModel.self)
ModelRegistry.register(handler: Arrow.self, for: ArrowModel.self) ModelRegistry.register(handler: Arrow.self, for: ArrowModel.self)