Pulsing Animation on layer
To scale each circle from their center, you should set the frame
of each layer to the bezier path's bounding box. Then you can just set a constant origin of CGPoint(x: radius, y: radius)
for the bezier path's arcCenter
.
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
drawCircles()
}
func drawCircles(){
let width = layerView.bounds.width
let halfOfView = width / 2
let radius = halfOfView / 3.5
let firstCenter = CGPoint(x: halfOfView / 2, y: halfOfView)
makeCircleLayer(center: firstCenter, radius: radius, color: .purple)
let secondCenter = CGPoint(x: halfOfView, y: halfOfView / 2)
makeCircleLayer(center: secondCenter, radius: radius, color: .yellow)
let thirdCenter = CGPoint(x: width * 0.75, y: halfOfView)
makeCircleLayer(center: thirdCenter, radius: radius, color: .brown)
let fourthCenter = CGPoint(x: halfOfView, y: width * 0.75)
makeCircleLayer(center: fourthCenter, radius: radius, color: .blue)
}
func makeCircleLayer(center: CGPoint, radius: CGFloat, color: UIColor) {
let layer = CAShapeLayer()
/// the frame is the actual frame/bounding box of the circle
layer.frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
/// path is relative to the frame
let path = UIBezierPath(arcCenter: CGPoint(x: radius, y: radius), radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
layer.path = path.cgPath
layer.fillColor = color.cgColor
layerView.layer.addSublayer(layer)
let scaling = CABasicAnimation(keyPath: "transform.scale")
scaling.toValue = 1.2
scaling.duration = 0.3
scaling.autoreverses = true
scaling.repeatCount = .infinity
layer.add(scaling, forKey: nil)
}
}
Result:
However, because your circles are just circles (not a complicated shape), you should just use UIView
's with a corner radius. The following code produces the same result, but is much cleaner.
class ViewController: UIViewController {
@IBOutlet weak var layerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
drawCircles()
}
func drawCircles() {
let width = layerView.bounds.width
let halfOfView = width / 2
let radius = halfOfView / 3.5
let firstCenter = CGPoint(x: halfOfView / 2, y: halfOfView)
makeCircleView(center: firstCenter, radius: radius, color: .purple)
let secondCenter = CGPoint(x: halfOfView, y: halfOfView / 2)
makeCircleView(center: secondCenter, radius: radius, color: .yellow)
let thirdCenter = CGPoint(x: width * 0.75, y: halfOfView)
makeCircleView(center: thirdCenter, radius: radius, color: .brown)
let fourthCenter = CGPoint(x: halfOfView, y: width * 0.75)
makeCircleView(center: fourthCenter, radius: radius, color: .blue)
}
func makeCircleView(center: CGPoint, radius: CGFloat, color: UIColor) {
let frame = CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2)
let circleView = UIView(frame: frame)
circleView.backgroundColor = color
circleView.layer.cornerRadius = radius
layerView.addSubview(circleView)
UIView.animate(withDuration: 0.3, delay: 0, options: [.repeat, .autoreverse]) {
circleView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2)
}
}
}
CABasicAnimation to emulate a 'pulse' effect animation on a non-circle shape
Scaling the width and height by the same factor is going to result in unequal spacing around the edges. You need to increase the layer's width and height by the same value. This is an addition operation, not multiplication. Now, for this pulsating effect you need to animate the layer's bounds.
If you want the spacing between the edges to be dynamic, then pick a scale factor and apply it to a single dimension. Whether you choose the width or the the height doesn't matter so long as it's only applied to one. Let's say you choose the width to grow by a factor of 1.1. Compute your target width, then compute the delta.
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
Once you have your delta, apply it to the layer's bounds in the x and the y dimension. Take advantage of the insetBy(dx:)
method to compute the resulting rectangle.
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
For clarity's sake, I've renamed your createScaleAnimation(view:)
method to createExpansionAnimation(view:)
. Tying it all together we have:
func createExpansionAnimation(view: UIView) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "bounds")
DispatchQueue.main.async {
let scaleFactor: CGFloat = 1.1
let targetWidth = view.bounds.size.width * scaleFactor
let delta = targetWidth - view.bounds.size.width
let targetBounds = self.bounds.insetBy(dx: -delta / 2, dy: -delta / 2)
anim.duration = 1.0
anim.fromValue = NSValue(cgRect: self.bounds)
anim.toValue = NSValue(cgRect: targetBounds)
}
return anim
}
Swift pulse animation non circle
Output:
Note: You can add CABasicAnimation
for opacity too in animationGroup
if this solution works for you.
extension UIView {
/// animate the border width
func animateBorderWidth(toValue: CGFloat, duration: Double) -> CABasicAnimation {
let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = layer.borderWidth
widthAnimation.toValue = toValue
widthAnimation.duration = duration
return widthAnimation
}
/// animate the scale
func animateScale(toValue: CGFloat, duration: Double) -> CABasicAnimation {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = 1.0
scaleAnimation.toValue = toValue
scaleAnimation.duration = duration
scaleAnimation.repeatCount = .infinity
scaleAnimation.isRemovedOnCompletion = false
scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
return scaleAnimation
}
func pulsate(animationDuration: CGFloat) {
var animationGroup = CAAnimationGroup()
animationGroup = CAAnimationGroup()
animationGroup.duration = animationDuration
animationGroup.repeatCount = Float.infinity
let newLayer = CALayer()
newLayer.bounds = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
newLayer.cornerRadius = self.frame.width/2
newLayer.position = CGPoint(x: self.frame.width/2, y: self.frame.height/2)
newLayer.cornerRadius = self.frame.width/2
animationGroup.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default)
animationGroup.animations = [animateScale(toValue: 6.0, duration: 2.0),
animateBorderWidth(toValue: 1.2, duration: animationDuration)]
newLayer.add(animationGroup, forKey: "pulse")
self.layer.cornerRadius = self.frame.width/2
self.layer.insertSublayer(newLayer, at: 0)
}
}
Usage:
class ViewController: UIViewController {
@IBOutlet weak var pulseView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
pulseView.pulsate(animationDuration: 1.0)
}
}
Swift Pulse Animation
This animation continually pulses from 0 to 1 opacity over 3 seconds:
let pulseAnimation = CABasicAnimation(keyPath: "opacity")
pulseAnimation.duration = 3
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 1
pulseAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = .greatestFiniteMagnitude
self.view.layer.add(pulseAnimation, forKey: nil)
How to do a native Pulse effect animation on a UIButton - iOS
CABasicAnimation *theAnimation;
theAnimation=[CABasicAnimation animationWithKeyPath:@"opacity"];
theAnimation.duration=1.0;
theAnimation.repeatCount=HUGE_VALF;
theAnimation.autoreverses=YES;
theAnimation.fromValue=[NSNumber numberWithFloat:1.0];
theAnimation.toValue=[NSNumber numberWithFloat:0.0];
[theLayer addAnimation:theAnimation forKey:@"animateOpacity"]; //myButton.layer instead of
Swift
let pulseAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
pulseAnimation.duration = 1
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 1
pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pulseAnimation.autoreverses = true
pulseAnimation.repeatCount = .greatestFiniteMagnitude
view.layer.add(pulseAnimation, forKey: "animateOpacity")
See the article "Animating Layer Content"
UIBezierPath pulse animation
I ended up dooing this:
UIBezierPath *linePath = [UIBezierPath bezierPath];
[linePath moveToPoint:startPoints];
[linePath addLineToPoint:endPoints];
//gradient layer for the line
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = CGRectMake(0, 0, 150.0, 1.0);
gradient.cornerRadius = 5.0f;
gradient.startPoint = CGPointMake(0.0, 0.5);
gradient.endPoint = CGPointMake(1.0, 0.5);
gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor clearColor] CGColor],(id)[[UIColor whiteColor] CGColor],(id)[[UIColor blueColor] CGColor],(id)[[UIColor clearColor] CGColor], nil];
[scrollViewContent.layer addSublayer:gradient];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
anim.path = [linePath CGPath];
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = 0;
anim.duration = 1;
[gradient addAnimation:anim forKey:@"gradient"];
Animating a pulsing UILabel?
You could probably do this with a CAAnimation
. I just pushed a little demo project I did a while ago onto github. It shows the olympic logo, and makes the rings fly around to a random location on the screen, then animate back to their original position. You could probably do something very similar, but obviously not using the position
property.
Here's the basic code to make one of the rings fly around:
CABasicAnimation * a = [CABasicAnimation animationWithKeyPath:@"position"];
[a setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[a setFromValue:[NSValue valueWithCGPoint:[layer position]]];
[a setToValue:[NSValue valueWithCGPoint:[self randomPoint]]];
[a setAutoreverses:YES];
[a setDuration:1.0];
[layer addAnimation:a forKey:nil];
Your fromValue
and toValue
values would be CGColorRefs
, and your animation keypath would be different, but the trick here is the setAutoreverses:YES
. This will animate from the fromValue
to the toValue
, and then back to the fromValue
. It's pretty darn handy. :)
https://github.com/davedelong/Demos/blob/master/OlympicRings
If that doesn't work (and it might not), then I might just layer two UILabels
on top of each other and animate their alpha values from 0.0 to 1.0 (or vice versa) at the same time.
How create circular pulse animation using xamarin.ios?
When you use DispatchQueue.MainQueue.DispatchAfter
to execute the animation, it's a synchronous function which will block the MainQueue, so you will always see one single layer. Change the code to DispatchAsync
would work:
NSTimer.CreateScheduledTimer(2.3, true, (obj) =>
{
DispatchQueue.MainQueue.DispatchAsync(() =>
{
animateCircularPulseAt(0);
});
});
NSTimer.CreateScheduledTimer(2.5, true, (obj) =>
{
DispatchQueue.MainQueue.DispatchAsync(() =>
{
animateCircularPulseAt(1);
});
});
I just tested and found this will be better:
NSTimer.CreateScheduledTimer(2.3, true, async (obj) =>
{
await Task.Delay(TimeSpan.FromMilliseconds(200));
animateCircularPulseAt(0);
await Task.Delay(TimeSpan.FromMilliseconds(400));
animateCircularPulseAt(1);
});
Related Topics
Uicollectionview Selected Cells Issue
Choppy Animation Swiftui Nested Views
Overlaying Image on Video Reduces Video Resolution
Is It a Good Way to Access Instance Variable with Self? If I Use a Lot
Gmsmarker Not Appearing on Map
Implementing a Custom Viewmodifier Where Output Is Conditional on Concrete View Type (Swiftui)
Realitykit - Updating Entity's Translation Returns Unexpected Values
Why I Can't Access My 3Rd Level Coredata Data in Swift
How to Determine Whether a Double Is an Integer
How to Fix Dylib with Invalid Signature
Communication Error When Uploading 90 Mb IPA File with Swift to Appstore
How to Populate Table Rows, Using a [String] Array Sent from iPhone by Watch Connectivity
Cocos2D Fcm Push Notification Not Working
How to Get and Save The Mixed of Multiple Audios in to Single Audio in Swift
Redirect Process Stdout to Apple System Log Facility in Swift
Programmatically Setting Texture in Scene Generated by Reality Composer