Reputation: 218
I made a custom progressbar for my app (following an article on medium), it works as intended but i have one problem, when i change the progress value then it jumps to fast! (dont get confused by the percent values below the bar, they are off, i know that)
i use setNeedsDisplay() to redraw my view.
I want the bar to animate smoothly, so in my case a bit slower.
this is the draw function of the bar:
override func draw(_ rect: CGRect) {
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height * 0.25).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
}
Here is the whole Class i used:
Anyone with an idea?
EDIT 1: Similar questions helped, but the result is not working properly. I aded this function to set the progress of the bar:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .forwards
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = 0.5
progressLayer.bounds = newBounds
gradientLayer.endPoint = CGPoint(x: progress, y: 0.5)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
}
And now the bar behaves like this:
Upvotes: 1
Views: 6625
Reputation: 218
After digging a while and a ton of testing, i came up with a solution, that suited my needs! Altough the above answer from DonMag was also working great (thanks for your effort), i wanted to fix what halfway worked. So the problem was, that the bar resized itself from the middle of the view. And on top, the position was also off for some reason.
First i set the position back to (0,0) so that the view started at the beginning (where it should). The next thing was the resizing from the middle, because with the position set back, the bar only animated to the half when i set it to 100%. After some tinkering and reading i found out, that changing the anchorPoint of the view would solve my problem. The default value was (0.5,0.5), changing it into (0,0) meant that it would only expand the desired direction.
After that i only needed to re-set the end of the gradient, so that the animation stays consistent between the different values. After all of this my bar worked like I imagined. And here is the result:
Here is the final code, i used to accomplish this:
func setProgress(to percent : CGFloat)
{
progress = percent
print(percent)
let duration = 0.5
let rect = self.bounds
let oldBounds = progressLayer.bounds
let newBounds = CGRect(origin: .zero, size: CGSize(width: rect.width * progress, height: rect.height))
let redrawAnimation = CABasicAnimation(keyPath: "bounds")
redrawAnimation.fromValue = oldBounds
redrawAnimation.toValue = newBounds
redrawAnimation.fillMode = .both
redrawAnimation.isRemovedOnCompletion = false
redrawAnimation.duration = duration
progressLayer.bounds = newBounds
progressLayer.position = CGPoint(x: 0, y: 0)
progressLayer.anchorPoint = CGPoint(x: 0, y: 0)
progressLayer.add(redrawAnimation, forKey: "redrawAnim")
let oldGradEnd = gradientLayer.endPoint
let newGradEnd = CGPoint(x: progress, y: 0.5)
let gradientEndAnimation = CABasicAnimation(keyPath: "endPoint")
gradientEndAnimation.fromValue = oldGradEnd
gradientEndAnimation.toValue = newGradEnd
gradientEndAnimation.fillMode = .both
gradientEndAnimation.isRemovedOnCompletion = false
gradientEndAnimation.duration = duration
gradientLayer.endPoint = newGradEnd
gradientLayer.add(gradientEndAnimation, forKey: "gradEndAnim")
}
Upvotes: 4
Reputation: 77462
I'm going to suggest a somewhat different approach.
First, instead of adding a sublayer as the gradient layer, we'll make the custom view's layer itself a gradient layer:
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
// then, in init
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
We'll set the gradient animation to the full size of the view... that will give it a consistent width and speed.
Next, we'll add a subview as a mask, instead of a layer-mask. That will allow us to animate its width independently.
class GradProgressView: UIView {
@IBInspectable var color: UIColor = .gray {
didSet { setNeedsDisplay() }
}
@IBInspectable var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
// this view will mask the percentage width
private let myMaskView = UIView()
// so we can calculate the new-progress-animation duration
private var curProgress: CGFloat = 0.0
public var progress: CGFloat = 0 {
didSet {
// calculate the change in progress
let changePercent = abs(curProgress - progress)
// if the change is 100% (i.e. from 0.0 to 1.0),
// we want the animation to take 1-second
// so, make the animation duration equal to
// 1-second * changePercent
let dur = changePercent * 1.0
// save the new progress
curProgress = progress
// calculate the new width of the mask view
var r = bounds
r.size.width *= progress
// animate the size of the mask-view
UIView.animate(withDuration: TimeInterval(dur), animations: {
self.myMaskView.frame = r
})
}
}
private var gradientLayer: CAGradientLayer!
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// use self.layer as the gradient layer
gradientLayer = self.layer as? CAGradientLayer
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.locations = [0.25, 0.5, 0.75]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [-0.3, -0.15, 0]
animation.toValue = [1, 1.15, 1.3]
animation.duration = 1.5
animation.isRemovedOnCompletion = false
animation.repeatCount = Float.infinity
gradientLayer.add(animation, forKey: nil)
myMaskView.backgroundColor = .white
mask = myMaskView
}
override func layoutSubviews() {
super.layoutSubviews()
// if the mask view frame has not been set at all yet
if myMaskView.frame.height == 0 {
var r = bounds
r.size.width = 0.0
myMaskView.frame = r
}
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
layer.cornerRadius = bounds.height * 0.25
}
}
Here's a sample controller class - each tap will cycle through a list of sample progress percentages:
class ExampleViewController: UIViewController {
let progView = GradProgressView()
let infoLabel = UILabel()
var idx: Int = 0
let testVals: [CGFloat] = [
0.75, 0.3, 0.95, 0.25, 0.5, 1.0,
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
[infoLabel, progView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
infoLabel.textColor = .white
infoLabel.textAlignment = .center
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
progView.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
progView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
progView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
progView.heightAnchor.constraint(equalToConstant: 40.0),
infoLabel.topAnchor.constraint(equalTo: progView.bottomAnchor, constant: 8.0),
infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
])
progView.color = #colorLiteral(red: 0.9932278991, green: 0.5762576461, blue: 0.03188031539, alpha: 1)
progView.gradientColor = #colorLiteral(red: 1, green: 0.8578521609, blue: 0.3033572137, alpha: 1)
// add a tap gesture recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(didTap(_:)))
view.addGestureRecognizer(t)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
didTap(nil)
}
@objc func didTap(_ g: UITapGestureRecognizer?) -> Void {
let n = idx % testVals.count
progView.progress = testVals[n]
idx += 1
infoLabel.text = "Auslastung \(Int(testVals[n] * 100))%"
}
}
Upvotes: 3