I have a vertical progress bar with an animating CAGradientLayer that shows "activity" to the user.
My problem is I can't get the animation to run top-down where the gradient line is parallel to x-axis. It currently animates left to right with the gradient line parallel to the y-axis.
I thought by adjusting the layer's startPoint and endPoint y-value it would do the trick, but the layer continues to animate from left to right.
Any guidance would be appreciated.
class ProgressBarView: UIView {
var color: UIColor = .red {
didSet { setNeedsDisplay() }
}
var gradientColor: UIColor = .white {
didSet { setNeedsDisplay() }
}
var progress: CGFloat = 0 {
didSet {
DispatchQueue.main.async { self.setNeedsDisplay() }
}
}
private let progressLayer = CALayer()
private let gradientLayer = CAGradientLayer()
private let backgroundMask = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayers()
createAnimation()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayers()
createAnimation()
}
override func draw(_ rect: CGRect) {
self.backgroundColor = UIColor.lightGray
gradientLayer.frame = rect
gradientLayer.colors = [color.cgColor, gradientColor.cgColor, color.cgColor]
gradientLayer.endPoint = CGPoint(x: 0.5, y: progress)
backgroundMask.path = UIBezierPath(roundedRect: rect, cornerRadius: 8).cgPath
layer.mask = backgroundMask
let progressRect = CGRect(x: 0, y: rect.height, width: rect.width, height: -(rect.height - (rect.height * progress)))
progressLayer.frame = progressRect
progressLayer.backgroundColor = UIColor.black.cgColor
}
private func setupLayers() {
layer.addSublayer(gradientLayer)
gradientLayer.mask = progressLayer
gradientLayer.locations = [0.35, 0.5, 0.65]
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0)
}
private func createAnimation() {
let flowAnimation = CABasicAnimation(keyPath: "locations")
flowAnimation.fromValue = [-0.3, -0.15, 0]
flowAnimation.toValue = [1, 1.15, 1.3]
flowAnimation.isRemovedOnCompletion = false
flowAnimation.repeatCount = Float.infinity
flowAnimation.duration = 1
gradientLayer.add(flowAnimation, forKey: "flowAnimation")
}
}
This should get you started...
On init:
Don't override
draw()... instead, inlayoutSubviews():When you update the
progressproperty, callsetNeedsLayout()to update the layer frames.Here's a modified version of your class:
and an example controller. Progress will start at 5% and increment by 10% with each tap anywhere on the screen:
("Percent Label" value can be off due to rounding.)
Edit - to clarify why it wasn't working..
So, why wasn't it working to begin with?
The original code was changing the gradient layer's
.endPointto a percentage of the height.However, the
.locationsare percentages of the.endPoint - .startPointvalue.Suppose the view is 400-pts tall... if we're at 25% and we set:
.startPoint = CGPoint(x: 0.5, y: 0.0).endPoint = CGPoint(x: 0.5, y: 0.25)the gradient will be calculated for 100-pts of height.
The
.locationsanimation goes from[-0.3, -0.15, 0]to[1, 1.15, 1.3]0 which is a total of 30% of the 100-pts (or 30-points). However, as soon as the location exceeds 1.0 it will fill out the rest of the layer's frame.Here's how it looks as we animate through:
I've adjusted the gray "progress" layer to be only half of the width -- at full width, it covers the beginning of the gradient animation:
Setting aside any discussion of putting the code inside
draw()orlayoutSubviews(), you can "fix" the issue by commenting out a single line in yourdraw()func:Now the actual gradient height will remain at 30% of the full height.
It wasn't clear initially what you wanted to do with the gray "progress" layer... here's a modified version using
layoutSubviews()instead ofdraw(). One big benefit is that the entire thing will automatically resize if the view frame changes: