CALayer animation for width - ios

I have one view in which need to add one layer. I successfully added the layer but now i want to do animation for it's width means layer start filling from "started" label and end at "awaiting..." label. I try to add animation with CABasicAnimation it start from beginning but it not properly fill. Please check video like for more understanding.
func startProgressAnimation()
{
self.ivStarted.isHighlighted = true
let layer = CALayer()
layer.backgroundColor = #colorLiteral(red: 0.4980392157, green: 0.8352941176, blue: 0.1921568627, alpha: 1)
layer.masksToBounds = true
layer.frame = CGRect.init(x: self.viewStartedAwaited.frame.origin.x, y: self.viewStartedAwaited.frame.origin.y, width: 0.0, height: self.viewStartedAwaited.frame.height)
self.ivStarted.layer.addSublayer(layer)
CATransaction.begin()
let boundsAnim:CABasicAnimation = CABasicAnimation(keyPath: "bounds.size.width")
boundsAnim.fromValue = NSNumber.init(value: 0.0)
boundsAnim.byValue = NSNumber.init(value: Float(self.viewStartedAwaited.frame.size.width / 2.0))
boundsAnim.toValue = NSNumber.init(value: Float(self.viewStartedAwaited.frame.size.width))
let anim = CAAnimationGroup()
anim.animations = [boundsAnim]
anim.isRemovedOnCompletion = false
anim.duration = 5
anim.fillMode = kCAFillModeBoth
CATransaction.setCompletionBlock{ [weak self] in
self?.ivAwaited.isHighlighted = true
}
layer.add(anim, forKey: "bounds.size.width")
CATransaction.commit()
}
Here is the video what i achieve with this code.
https://streamable.com/h6tpp
Not Proper
Proper image

Just set your
layer.anchorPoint = CGPoint(x: 0, y: 0)
After this it will work according to your requirement.
The default anchorpoint of (0.5,0.5) means that the growing or shrinking of width happens evenly on both sides. Anchorpoint of (0,0) means the layer is essentially pinned to the top left, so whatever new width it gets, the change happens on the right. That is, it matters not whether your animation key path is 'bounds' or 'bounds.size.width'. The anchor point is what determines where the change happens

Related

How to position Layers in swift programmatically?

I am trying to position a circular progress bar but I am failing constantly. I guess I am not understanding the concept of frames and views and bounds. I found this post on stack overflow that shows me exactly how to construct a circular progress bar.
However, in the post, a circleView was created as UIView using CGRect(x: 100, y: 100, width: 100, height: 100)
In my case, manually setting the x and y coordinates is obv a big no. So the circleView has to be in the centre of it's parent view.
Here is my view hierarchy:
view -> view2ForHomeController -> middleView -> circleView
So everything is positioned using auto layout. Here is the problem:The circleView is adding properly and it positions itself at the x and y value I specify but how can I specify the x and y values of the center of middleView. I tried the following but the center values are returned as 0.0, 0.0.
self.view.addSubview(self.view2ForHomeController)
self.view2ForHomeController.fillSuperview()
let xPosition = self.view2ForHomeController.middleView.frame.midX
let yPostion = self.view2ForHomeController.middleView.frame.midY
print(xPosition) // ------- PRINTS ZERO
print(yPostion) // ------- PRINTS ZERO
let circle = UIView(frame: CGRect(x: xPosition, y: yPostion, width: 100, height: 100))
circle.backgroundColor = UIColor.green
self.view2ForHomeController.middleView.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = self.view.bounds
let lineWidth: CGFloat = 10
let rectFofOval = CGRect(x: lineWidth / 2, y: lineWidth / 2, width: circle.bounds.width - lineWidth, height: circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalIn: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.white.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 10.0
progressCircle.frame = self.view.bounds
progressCircle.lineCap = CAShapeLayerLineCap(rawValue: "round")
circle.layer.addSublayer(progressCircle)
circle.transform = circle.transform.rotated(by: CGFloat.pi/2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
progressCircle.add(animation, forKey: nil)
Frame is in terms of superview/superlayer coordinates. If you are going to say
circle.layer.addSublayer(progressCircle)
then you must give progressCircle a frame in terms of the bounds of circle.layer.
As for centering, in general, to center a sublayer in its superlayer you say:
theSublayer.position = CGPoint(
x:theSuperlayer.bounds.midX,
y:theSuperlayer.bounds.midY)
And to center a subview in its superview you say:
theSubview.center = CGPoint(
x:theSuperview.bounds.midX,
y:theSuperview.bounds.midY)
The bounds are the layer/view's coordinates in its local coordinate system. The frame is the layer/view's coordinates in its parents coordinate system. Since layers do not participate in auto layout, you should implement UIViewController.viewDidLayoutSubviews (or UIView.layoutSubLayers) and set the frame of the layer to the bounds of its super layer's view (the backing layer of a UIView essentially is the CoreGraphics version of that view).
This also means you need to recompute your path in they layout method. if that is expensive then draw your path in unit space and use the transform of the layer to scale it up to the view's dimensions instead.
The solution is to first add the subviews and then create the entire circular progress view at
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
// Add and position the circular progress bar and its layers here
}

How to make UIBezierPath to fill from the center

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.
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.

UIView fill animation in iOS Swift?

I want to fill the color of view from left to right as fill animation. I have been using this code to get the effect but somehow it doesn't show any animation.i am using below code.
UIView.animate(withDuration: 2.0, delay: 0.2, options:.curveLinear, animations: {
view!.backgroundColor = Constant.AppColor.viewBottomFocus
}, completion:nil)
please tell me how can i achieve this?
I don't think you can fill a color with left to right animation with simple UIView blocks. You can ofcourse change width or trailing constraint of the view and it will appear that the view is changing its color. But, I dont think thats the ideal way to do it.
To achieve what you want I guess you have to use gradient layer and animate the locations:
class AnimatingV:UIView {
func animate() {
let layer = CAGradientLayer()
let startLocations = [0, 0]
let endLocations = [1, 2]
layer.colors = [UIColor.red.cgColor, UIColor.white.cgColor]
layer.frame = self.frame
layer.locations = startLocations as [NSNumber]
layer.startPoint = CGPoint(x: 0.0, y: 1.0)
layer.endPoint = CGPoint(x: 1.0, y: 1.0)
self.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "locations")
anim.fromValue = startLocations
anim.toValue = endLocations
anim.duration = 2.0
layer.add(anim, forKey: "loc")
layer.locations = endLocations as [NSNumber]
}
}
let animatingV = AnimatingV(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
animatingV.backgroundColor = UIColor.yellow
animatingV.animate()
PlaygroundPage.current.liveView = animatingV
Create UIView in storyboard and make its subclass of UIProgressView
then use this code to fill view , you can use animated fill as well by calling this function in scheduler
//progressView is view that you have create in storyboard
progressView.progress = .25; // .25 is the fill rate of total view
progressView.trackTintColor = [UIColor whiteColor];
progressView.progressTintColor = [UIColor redColor];

CABasicAnimation not scaling around center

I want to perform opacity And Scale effect at same time my animation work perfect but it's position is not proper. i want to perform animation on center.
This is my code.
btn.backgroundColor = UIColor.yellowColor()
let stroke = UIColor(red:236.0/255, green:0.0/255, blue:140.0/255, alpha:0.8)
let pathFrame = CGRectMake(24, 13, btn.bounds.size.height/2, btn.bounds.size.height/2)
let circleShape1 = CAShapeLayer()
circleShape1.path = UIBezierPath(roundedRect: pathFrame, cornerRadius: btn.bounds.size.height/2).CGPath
circleShape1.position = CGPoint(x: 2, y: 2)
circleShape1.fillColor = stroke.CGColor
circleShape1.opacity = 0
btn.layer.addSublayer(circleShape1)
circleShape1.anchorPoint = CGPoint(x: 0.5, y: 0.5)
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSValue(CATransform3D: CATransform3DIdentity)
scaleAnimation.toValue = NSValue(CATransform3D: CATransform3DMakeScale(2.0, 2.0, 1))
let alphaAnimation = CABasicAnimation(keyPath: "opacity")
alphaAnimation.fromValue = 1
alphaAnimation.toValue = 0
CATransaction.begin()
let animation = CAAnimationGroup()
animation.animations = [scaleAnimation, alphaAnimation]
animation.duration = 1.5
animation.repeatCount = .infinity
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
circleShape1.addAnimation(animation, forKey:"Ripple")
CATransaction.commit()
The problem is that you are not grappling with frames correctly. You say:
let circleShape1 = CAShapeLayer()
But you have forgotten to give circleShape1 a frame! Thus, its size is zero, and very weird things happen when you animate it. Your job in the very next line should be to assign circleShape1 a frame. Example:
circleShape1.frame = pathFrame
That may or may not be the correct frame; it probably isn't. But you need to figure that out.
Then, you need to fix the frame of your Bezier path in terms of the shape layer's bounds:
circleShape1.path = UIBezierPath(roundedRect: circleShape1.bounds // ...
I have never worked with sublayers so I would make it with a subview instead, makes the code a lot easier:
btn.backgroundColor = UIColor.yellow
let circleShape1 = UIView()
circleShape1.frame.size = CGSize(width: btn.frame.height / 2, height: btn.frame.height / 2)
circleShape1.center = CGPoint(x: btn.frame.width / 2, y: btn.frame.height / 2)
circleShape1.layer.cornerRadius = btn.frame.height / 4
circleShape1.backgroundColor = UIColor(red:236.0/255, green:0.0/255, blue:140.0/255, alpha:0.8)
circleShape1.alpha = 1
btn.addSubview(circleShape1)
UIView.animate(withDuration: 1,
delay: 0,
options: [.repeat, .curveLinear],
animations: {
circleShape1.transform = CGAffineTransform(scaleX: 5, y: 5)
circleShape1.alpha = 0.4
}, completion: nil)
You need to create path frame with {0,0} position, and set correct frame to the Layer:
let pathFrame = CGRectMake(0, 0, btn.bounds.size.height/2, btn.bounds.size.height/2)
....
circleShape1.frame = CGRect(x: 0, y: 0, width: pathFrame.width, height: pathFrame.height)
circleShape1.position = CGPoint(x: 2, y: 2)
If you want to create path with {13,24} position you need to change width and height in layer. Your shape should be in center of layer.

Incorrect position of CAShapeLayer

I have a UIView called viewProgress. It is the white box in the image. I want a circular progress bar, which is the green circle in the image. The progress bar should stay within the white box, but as you can see it is way off.
How do I make it stay inside the viewProgress?
Here you have my animation function:
func animateView() {
let circle = viewProgress // viewProgress is a UIView
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(arcCenter: circle.center, radius: circle.bounds.midX, startAngle: -CGFloat(M_PI_2), endAngle: CGFloat(3.0 * M_PI_2), clockwise: true)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.whiteColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 10.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 0.4
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
}
And I'm just calling the function inside viewDidLoad()
Basically u have few problems:
incorrect position of your layer - frame should be equal to bounds of parent layer in parentView
you animation will play only once - if u want to make it infinite add something like animation.repeatCount = MAXFLOAT
to make sure that all size correct - in layoutSubviews recreate layers, with removing previously created one
UIBezierPath(arcCenter... will start path from rightSide, u can also use simpler variant like UIBezierPath(ovalInRect. With this in mind u also need to rotate layer for M_PI_2 if u want to make start of drawing in the very top of circle
U should also take in mind that with line width 10, half of u'r circle path will be catted by clipSubviews of parentView. To prevent this use modified parentLayer rect
U can also maybe want to improve visualization of lineCap, buy setting it to "round" style
progressCircle.addAnimation(animation, forKey: "ani") key for anim not required if u dont want to get completion event from animation - i your case i see there is no delegate and no removeOnCompletion flag setted to false, so make assumption that u don`t actually need it
Keeping this all in mind code can be like :
let circle = UIView(frame: CGRectMake(100, 100, 100, 100)) // viewProgress is a UIView
circle.backgroundColor = UIColor.greenColor()
view.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = view.bounds
let lineWidth:CGFloat = 10
let rectFofOval = CGRectMake(lineWidth / 2, lineWidth / 2, circle.bounds.width - lineWidth, circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalInRect: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.whiteColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 10.0
progressCircle.frame = view.bounds
progressCircle.lineCap = "round"
circle.layer.addSublayer(progressCircle)
circle.transform = CGAffineTransformRotate(circle.transform, CGFloat(-M_PI_2))
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
progressCircle.addAnimation(animation, forKey: nil)
and result actually like:
Note:
If u want to animate rotation of animated layer u should also add additional animation in to anim group

Resources