Reputation: 91
How do I make a UIBezierPath that starts drawing from the center and expands to the left and right?
My current solution is not using UIBezierPath but rather a UIView inside another UIView. The children view is centered inside of it's parent and I adjust child's width when I need it to expand. However, this solution is far from perfect because the view is located inside of UIStackView and the stack view is in UICollectionView cell and it sometimes does not get the width correctly.
override func layoutSubviews() {
super.layoutSubviews()
progressView.cornerRadius = cornerRadius
clipsToBounds = true
addSubview(progressView)
NSLayoutConstraint.activate([
progressView.topAnchor.constraint(equalTo: topAnchor),
progressView.bottomAnchor.constraint(equalTo: bottomAnchor),
progressView.centerXAnchor.constraint(equalTo: centerXAnchor)
])
let width = frame.width
let finalProgress = CGFloat(progress) * width
let widthConstraint = progressView.widthAnchor.constraint(equalToConstant: finalProgress)
widthConstraint.priority = UILayoutPriority(rawValue: 999)
widthConstraint.isActive = true
}
the progress is a class property of type double and when it gets set I call layoutIfNeeded.
I also tried calling setNeedsLayout and layoutIfNeeded but it does not solve the issue. SetNeedsDisplay and invalidateIntrinzicSize do not work either.
Now, I would like to do it using bezier path, but the issue is that it starts from a given point and draws from left to right. The question is: how do I make a UIBezierPath that is centered and when I increase the width it expands (stays in center)
Here is the image of what I want to achieve.
Upvotes: 0
Views: 512
Reputation: 437422
There are quite a few options. But here are a few:
CABasicAnimation
of the strokeStart
and strokeEnd
of CAShapeLayer
, namely:
Create CAShapeLayer
whose path
is the final UIBezierPath
(full width), but whose strokeStart
and strokeEnd
are both 0.5 (i.e. start half-way, and finish half-way, i.e. nothing visible yet).
func configure() {
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineCap = .round
setProgress(0, animated: false)
}
func updatePath() {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.minX + lineWidth, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.maxX - lineWidth, y: bounds.midY))
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
}
Then, in CABasicAnimation
animate the change of the strokeStart
to 0
and the strokeEnd
to 1
to achieve the animation growing both left and right from the middle. For example:
func setProgress(_ progress: CGFloat, animated: Bool = true) {
let percent = max(0, min(1, progress))
let strokeStart = (1 - percent) / 2
let strokeEnd = 1 - (1 - percent) / 2
if animated {
let strokeStartAnimation = CABasicAnimation(keyPath: "strokeStart")
strokeStartAnimation.fromValue = shapeLayer.strokeStart
strokeStartAnimation.toValue = strokeStart
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.fromValue = shapeLayer.strokeEnd
strokeEndAnimation.toValue = strokeEnd
let animation = CAAnimationGroup()
animation.animations = [strokeStartAnimation, strokeEndAnimation]
shapeLayer.strokeStart = strokeStart
shapeLayer.strokeEnd = strokeEnd
layer.add(animation, forKey: nil)
} else {
shapeLayer.strokeStart = strokeStart
shapeLayer.strokeEnd = strokeEnd
}
}
Animation of the path
of a CAShapeLayer
Have method that creates the UIBezierPath
:
func setProgress(_ progress: CGFloat, animated: Bool = true) {
self.progress = progress
let cgPath = path(for: progress).cgPath
if animated {
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = shapeLayer.path
animation.toValue = cgPath
shapeLayer.path = cgPath
layer.add(animation, forKey: nil)
} else {
shapeLayer.path = cgPath
}
}
Where
func path(for progress: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
let percent = max(0, min(1, progress))
let width = bounds.width - lineWidth
path.move(to: CGPoint(x: bounds.midX - width * percent / 2 , y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX + width * percent / 2, y: bounds.midY))
return path
}
When you create the CAShapeLayer
call setProgress
with value of 0
and no animation; and
When you want to animate it out, then call setProgress
with value of 1
but with animation.
Abandon UIBezierPath
/CAShapeLayer
approaches and use UIView
and UIView
block-based animation:
Create short UIView
with a backgroundColor
and whose layer has a cornerRadius
which is half the height of the view.
Define constraints such that the view has a zero width. E.g. you might define the centerXanchor
and so that it’s placed where you want it, and with a widthAnchor
is zero.
Animate the constraints by setting the existing widthAnchor
’s constant
to whatever width you want and place layoutIfNeeded
inside a UIView
animation block.
Upvotes: 2