Pulsing Animation on Layer

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:

Each circle scales from its center

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:

Sample Image

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



Leave a reply



Submit