I'm working on a document viewer. The document is displayed inside a UIScrollView so that it can be scrolled and zoomed. I need to draw a border around the document in order to separate it visually from the background of the UIScrollView. The border must not be zoomed together with the document -- it should maintain a constant thickness regardless of the zoom scale.
My current setup consists of a UIScrollView with two UIView children -- one for the document, and one for the border. I've overriden viewForZoomingInScrollView: to return the document view. I've also overridden layoutSubviews to center the document view (in case it's smaller than the UIScrollView) and then resize and position the border view behind it so that it looks like a frame. This works OK when the user is scrolling and zooming manually. But when I use zoomToRect:animated: to zoom programatically, layoutSubviews is called before the animation starts and my border view gets resized immediately with the document view catching up a bit later.
Clarification: The border needs to be tightly fitting around the document view and not around the UIScrollView itself.
Sample Code :
yourScrollView.layer.cornerRadius=8.0f;
yourScrollView.layer.masksToBounds=YES;
yourScrollView.layer.borderColor=[[UIColor redColor]CGColor];
yourScrollView.layer.borderWidth= 1.0f;
Don't Forget : #Import <QuartzCore/QuartzCore.h>
First you need to Import QuartzCore framework to your App.
then import that .h file on which class where you want to set the border.
like this.
#import <QuartzCore/QuartzCore.h>
Setup for Border.
ScrollView.layer.cornerRadius=5.0f;
ScrollView.layer.masksToBounds=YES;
ScrollView.layer.borderColor=[[UIColor redColor]CGColor];
ScrollView.layer.borderWidth= 4.0f;
check this one really helpful to you.
Finally, I was able to fix the problem with animated zooming. My setup is the same as described in the question. I just added some code to my layoutSubview implementation in order to detect any running UIScrollView animation and match it with a similar animation for resizing the border.
Here is the code:
- (void)layoutSubviews
{
[super layoutSubviews];
// _pageView displays the document
// _borderView represents the border around it
. . .
// _pageView is now centered -- we have to move/resize _borderView
// layers backing a view are not implicitly animated
// the following two lines work just fine if we don't need animation
_borderView.layer.bounds = CGRectMake(0.0, 0.0, frameToCenter.size.width + 2.0 * 15.0, frameToCenter.size.height + 2.0 * 15.0);
_borderView.layer.position = _pageView.center;
if (_pageView.layer.animationKeys.count > 0)
{
// UIScrollView is animating its content (_pageView)
// so we need to setup a matching animation for _borderView
[CATransaction begin];
CAAnimation *animation = [_pageView.layer animationForKey:[_pageView.layer.animationKeys lastObject]];
CFTimeInterval beginTime = animation.beginTime;
CFTimeInterval duration = animation.duration;
if (beginTime != 0.0) // 0.0 means the animation starts now
{
CFTimeInterval currentTime = [_pageView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
duration = MAX(beginTime + duration - currentTime, 0.0);
}
[CATransaction setAnimationDuration:duration];
[CATransaction setAnimationTimingFunction:animation.timingFunction];
// calculate the initial state for _borderView animation from _pageView presentation layer
CGPoint presentationPos = [_pageView.layer.presentationLayer position];
CGRect presentationBounds = [_pageView.layer.presentationLayer frame];
presentationBounds.origin = CGPointZero;
presentationBounds.size.width += 2.0 * 15.0;
presentationBounds.size.height += 2.0 * 15.0;
CABasicAnimation *boundsAnim = [CABasicAnimation animationWithKeyPath:#"bounds"];
boundsAnim.fromValue = [NSValue valueWithCGRect:presentationBounds];
boundsAnim.toValue = [NSValue valueWithCGRect:_borderView.layer.bounds];
[_borderView.layer addAnimation:boundsAnim forKey:#"bounds"];
CABasicAnimation *posAnim = [CABasicAnimation animationWithKeyPath:#"position"];
posAnim.fromValue = [NSValue valueWithCGPoint:presentationPos];
posAnim.toValue = [NSValue valueWithCGPoint:_borderView.layer.position];
[_borderView.layer addAnimation:posAnim forKey:#"position"];
[CATransaction commit];
}
}
It looks hacky but it works. I wish I didn't have to reverse engineer UIScrollView in order to make a simple border look good during animation...
Import QuartzCore framework:
#import <QuartzCore/QuartzCore.h>
Now Add color to its view:
[scrollViewObj.layer setBorderColor:[[UIColor redColor] CGColor]];
[scrollViewObj.layer setBorderWidth:2.0f];
Related
In a UIView animation for a view, you can animate its subviews being laid out by including UIViewAnimationOptionLayoutSubviews in the options parameter of [UIView animateWithDuration:delay:options:animations:completion:]. However, I cannot find a way to animate it laying out its sublayers when they are not some view's backing layer; they would just jump into place to match the new bounds of the view. Since I'm working with layers and not views, it seems like I have to use Core Animation instead of UIView animation, but I don't know how (and when) to do this such that the layer animation would match up to the view animation.
That's my basic question. Read more if you want to know the concrete thing I'm trying to accomplish.
I've created a view with a dotted border by adding a CAShapeLayer to the view's layer (see this stackoverflow question: Dashed line border around UIView). I adjust the path of the CAShapeLayer to match the bounds of the view in layoutSubviews.
This works, but there is one cosmetic issue: when the view's bounds is animated in a UIView animation (like during rotation), the dotted border jumps to the new bounds of the view instead of smoothly animating to it as the view animates its bounds. That is, the right and bottom parts of the dotted border do not respectively stay hugged to the right and bottom parts of the view as the view animates. How can I get the dotted border from the CAShapeLayer to animate alongside the view as it animates its bounds?
What I'm doing so far is attaching a CABasicAnimation to the CAShapeLayer:
- (void)layoutSubviews
{
[super layoutSubviews];
self.borderLayer.path = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
self.borderLayer.frame = self.bounds;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
[self.borderLayer addAnimation:pathAnimation forKey:nil];
}
This does cause the dotted border to animate, but it does not have the right timing function and animation duration to match the view animation. Also, sometimes, we don't want the dotted border to animate like, when the view first does layout, the border should not animate from some old path to the new correct path; it should just appear.
You might have found the answer already, but I also faced similar problems recently, since solved it, I will post the answer.
In - layoutSubViews method, you can get current UIView animation as backing layer's CAAnimation with - animationForKey: method.
Using this, You can implement - layoutSubviews like:
- (void)layoutSubviews {
[super layoutSubviews];
// get current animation for bounds
CAAnimation *anim = [self.layer animationForKey:#"bounds"];
[CATransaction begin];
if(anim) {
// animating, apply same duration and timing function.
[CATransaction setAnimationDuration:anim.duration];
[CATransaction setAnimationTimingFunction:anim.timingFunction];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
[self.borderLayer addAnimation:pathAnimation forKey:#"path"];
}
else {
// not animating, we should disable implicit animations.
[CATransaction disableActions];
}
self.borderLayer.path = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
self.borderLayer.frame = self.bounds;
[CATransaction commit];
}
I'm trying to animate a custom UIView's bounds while also keeping its layer the same size as its parent view. To do that, I'm trying to animate the layers bounds alongside its parent view. I need the layer to call drawLayer:withContext AS its animating so my custom drawing will change size correctly along with the bounds.
drawLayer is called correctly and draws correctly before I start the animation. But I can't get the layer to call its drawLayer method on EACH step of the bounds animation. Instead, it just calls it ONCE, jumping immediately to the "end bounds" at the final frame of the animation.
// self.bg is a property pointing to my custom UIView
self.bg.layer.needsDisplayOnBoundsChange = YES;
self.bg.layer.mask.needsDisplayOnBoundsChange = YES;
[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseOut|UIViewAnimationOptionAutoreverse|UIViewAnimationOptionRepeat animations:^{
[CATransaction begin];
self.bg.layer.bounds = bounds;
self.bg.layer.mask.bounds = bounds;
[CATransaction commit];
self.bg.bounds = bounds;
} completion:nil];
Why doesn't the bounds report a change AS its animating (not just the final frame)? What am I doing wrong?
This might or might not help...
Many people are unaware that Core Animation has a supercool feature that allows you to define your own layer properties in such a way that they can be animated. An example I use is to give a CALayer subclass a thickness property. When I animate it with Core Animation...
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"thickness"];
ba.toValue = #10.0f;
ba.autoreverses = YES;
[lay addAnimation:ba forKey:nil];
...the effect is that drawInContext: or drawLayer:... is called repeatedly throughout the animation, allowing me to change repeatedly the way the layer is drawn in accordance with its current thickness property value at each moment (an intermediate value in the course of the animation).
It seems to me that that might be the sort of thing you're after. If so, you can find a working downloadable example here:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch17p498customAnimatableProperty
Discussion (from my book) here:
http://www.apeth.com/iOSBook/ch17.html#_making_a_property_animatable
This is because the layer you are drawing to is not the same layer as the one displayed on the screen.
When you animate a layer property it will immediately be set to its final value in the model layer, (as you have noticed), and the actual animation is done in the presentation layer.
You can access the presentation layer and see the actual values of the animated properties:
CALayer *presentationLayer = (CALayer *)[self.bg.layer presentationLayer];
...
Since you haven't provided your drawLayer:withContext method, it's unclear what you want to draw during the animation, but if you want to animate custom properties, here is a good tutorial for doing that.
Firstly, the layer of a layer backed (or hosting) view is always resized to fit the bounds of its parent view. If you set the view to be the layers delegate then the view will receive drawLayer:inContext: at each frame. Of course you must ensure that If your layer has needsDisplayOnBoundsChange == YES.
Here is an example (on the Mac) of resizing a window, which then changes the path of the underlying layer.
// My Nib contains one view and one button.
// The view has a MPView class and the button action is resizeWindow:
#interface MPView() {
CAShapeLayer *_hostLayer;
CALayer *_outerLayer;
CAShapeLayer *_innerLayer;
}
#end
#implementation MPView
- (void)awakeFromNib
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
_hostLayer = [CAShapeLayer layer];
_hostLayer.backgroundColor = [NSColor blackColor].CGColor;
_hostLayer.borderColor = [NSColor redColor].CGColor;
_hostLayer.borderWidth = 2;
_hostLayer.needsDisplayOnBoundsChange = YES;
_hostLayer.delegate = self;
_hostLayer.lineWidth = 4;
_hostLayer.strokeColor = [NSColor greenColor].CGColor;
_hostLayer.needsDisplayOnBoundsChange = YES;
self.layer = _hostLayer;
self.wantsLayer = YES;
[CATransaction commit];
[self.window setFrame:CGRectMake(100, 100, 200, 200) display:YES animate:NO];
}
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == _hostLayer) {
CGSize size = layer.bounds.size;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, size.width, size.height);
_hostLayer.path = path;
CGPathRelease(path);
}
}
- (IBAction)resizeWindow:(id)sender
{
[self.window setFrame:CGRectMake(100, 100, 1200, 800) display:YES animate:YES];
}
#end
I'm using a pair of CALayers as image masks, allowing me to animate a bulb filling or emptying at a set speed while also following the current touch position. That is, one mask jumps to follow the touch and the other slides to that position. Since I use an explicit animation I'm forced to set the position of the mask sliding mask when I add the animation. This means that if I start a fill and then start an empty before the fill completes the empty will begin from the completed fill position (the opposite is also true).
Is there a way to get the position of the animation, set the position at each step of the animation, or to have the new animation begin from the current state of the active animation?
The code handling the animating is below:
- (void)masksFillTo:(CGFloat)height {
// Clamp the height we fill to inside the bulb. Remember Y gets bigger going down.
height = MIN(MAX(BULB_TOP, height), BULB_BOTTOM);
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// Find the bottom of the transparent mask to determine where the solid fill
// is sitting. Then find how far that fill needs to move.
// TODO: This works with the new set position, so overriding old anime doesn't work
CGFloat bottom = transMask.frame.origin.y + transMask.frame.size.height;
// If the target is above the bottom of the solid, we want to fill up.
// This means the empty mask jumps and the transparent mask slides.
CALayer *jumper;
CALayer *slider;
if (bottom - targetY >= 0) {
jumper = emptyMask;
slider = transMask;
// We need to reset the bottom to the emptyMask
bottom = emptyMask.frame.origin.y + emptyMask.frame.size.height;
} else {
jumper = transMask;
slider = emptyMask;
}
[jumper removeAllAnimations];
[slider removeAllAnimations];
CGFloat dy = bottom - targetY;
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[jumper setPosition:CGPointMake(jumper.position.x, jumper.position.y - dy)];
[self slideMaskFillTo:height withMask:slider]; // Do this inside here or an odd flash glitch appears.
[CATransaction commit];
}
// TODO: Always starts from new position, even if animation hasn't reached it.
- (void)slideMaskFillTo:(CGFloat)height withMask:(CALayer *)slider {
// We can find the target Y location by subtracting the Y value for the top of the
// bulb from the height.
CGFloat targetY = height - BULB_TOP;
// We then find the bottom of the mask.
CGFloat bottom = slider.frame.origin.y + slider.frame.size.height;
CGFloat dy = bottom - targetY;
// Do the animation. Animating with duration doesn't appear to work properly.
// Apparently "When modifying layer properties from threads that don’t have a runloop,
// you must use explicit transactions."
CABasicAnimation *a = [CABasicAnimation animationWithKeyPath:#"position"];
a.duration = (dy > 0 ? dy : -dy) / PXL_PER_SEC; // Should be 2 seconds for a full fill
a.fromValue = [NSValue valueWithCGPoint:slider.position];
CGPoint newPosition = slider.position;
newPosition.y -= dy;
a.toValue = [NSValue valueWithCGPoint:newPosition];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[slider addAnimation:a forKey:#"colorize"];
// Update the actual position
slider.position = newPosition;
}
And an example of how this is called. Notice this means it can be called mid-animation.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView:self.view];
[self masksFillTo:point.y];
}
If anyone finds it relevant, this is the creation of the images and masks.
// Instantiate the different bulb images - empty, transparent yellow, and solid yellow. This
// includes setting the frame sizes. This approach found at http://stackoverflow.com/a/11218097/264775
emptyBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light.png"]];
transBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-moving.png"]];
solidBulb = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"Light-on.png"]];
[emptyBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[transBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[solidBulb setFrame:CGRectMake(10, BULB_TOP, 300, BULB_HEIGHT)]; // 298 x 280
[self.view addSubview:solidBulb]; // Empty on top, then trans, then solid.
[self.view addSubview:transBulb];
[self.view addSubview:emptyBulb];
// Create a mask for the empty layer so it will cover the other layers.
emptyMask = [CALayer layer];
[emptyMask setContentsScale:emptyBulb.layer.contentsScale]; // handle retina scaling
[emptyMask setFrame:emptyBulb.layer.bounds];
[emptyMask setBackgroundColor:[UIColor blackColor].CGColor];
emptyBulb.layer.mask = emptyMask;
// Also create a mask for the transparent image.
transMask = [CALayer layer];
[transMask setContentsScale:transBulb.layer.contentsScale]; // handle retina scaling
[transMask setFrame:transBulb.layer.bounds];
[transMask setBackgroundColor:[UIColor blackColor].CGColor];
transBulb.layer.mask = transMask;
Was just led to a solution via this answer. If one looks in the right part of the docs, you'll find the following:
- (id)presentationLayer
Returns a copy of the layer containing all properties as they were at the start of the current transaction, with any active animations applied.
So if I add this code before I first check the transparent mask location (aka solid level), I grab the current animated position and can switch between fill up and fill down.
CALayer *temp;
if (transMask.animationKeys.count > 0) {
temp = transMask.presentationLayer;
transMask.position = temp.position;
}
if (emptyMask.animationKeys.count > 0) {
temp = emptyMask.presentationLayer;
emptyMask.position = temp.position;
}
layer.presentation() makes copy of original layer each time you get it, and you need to override init(layer: Any) for every custom CALayer you have.
Another possible way to start from current state is to set animation's beginTime using current animation's beginTime. It allows to start new animation with offset.
In your case it could be (Swift):
if let currentAnimation = slider.animation(forKey: "colorize") {
let currentTime = CACurrentMediaTime()
let animationElapsedTime = currentAnimation.beginTime + currentAnimation.duration - currentTime
a.beginTime = currentTime - animationElapsedTime
}
However this works great for linear timings, but for others it could be difficult to calculate the proper time.
I'm trying to create a UIView which shows a semitransparent circle with an opaque border inside its bounds. I want to be able to change the bounds in two ways - inside a -[UIView animateWithDuration:animations:] block and in a pinch gesture recogniser action which fires several times a second. I've tried three approaches based on answers elsewhere on SO, and none are suitable.
Setting the corner radius of the view's layer in layoutSubviews gives smooth translations, but the view doesn't stay circular during animations; it seems that cornerRadius isn't animatable.
Drawing the circle in drawRect: gives a consistently circular view, but if the circle gets too big then resizing in the pinch gesture gets choppy because the device is spending too much time redrawing the circle.
Adding a CAShapeLayer and setting its path property in layoutSublayersOfLayer, which doesn't animate inside UIView animations since path isn't implicitly animatable.
Is there a way for me to create a view which is consistently circular and smoothly resizable? Is there some other type of layer I could use to take advantage of the hardware acceleration?
UPDATE
A commenter has asked me to expand on what I mean when I say that I want to change the bounds inside a -[UIView animateWithDuration:animations:] block. In my code, I have a view which contains my circle view. The circle view (the version that uses cornerRadius) overrides -[setBounds:] in order to set the corner radius:
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = fminf(bounds.size.width, bounds.size.height) / 2.0;
[super setBounds:bounds];
}
The bounds of the circle view are set in -[layoutSubviews]:
-(void)layoutSubviews
{
// some other layout is performed and circleRadius and circleCenter are
// calculated based on the properties and current size of the view.
self.circleView.bounds = CGRectMake(0, 0, circleRadius*2, circleRadius*2);
self.circleView.center = circleCenter;
}
The view is sometimes resized in animations, like so:
[UIView animateWithDuration:0.33 animations:^(void) {
myView.frame = CGRectMake(x, y, w, h);
[myView setNeedsLayout];
[myView layoutIfNeeded];
}];
but during these animations, if I draw the circle view using a layer with a cornerRadius, it goes funny shapes. I can't pass the animation duration in to layoutSubviews so I need to add the right animation within -[setBounds].
As the section on Animations in the "View Programming Guide for iOS" says
Both UIKit and Core Animation provide support for animations, but the level of support provided by each technology varies. In UIKit, animations are performed using UIView objects
The full list of properties that you can animate using either the older
[UIView beginAnimations:context:];
[UIView setAnimationDuration:];
// Change properties here...
[UIView commitAnimations];
or the newer
[UIView animateWithDuration:animations:];
(that you are using) are:
frame
bounds
center
transform (CGAffineTransform, not the CATransform3D)
alpha
backgroundColor
contentStretch
What confuses people is that you can also animate the same properties on the layer inside the UIView animation block, i.e. the frame, bounds, position, opacity, backgroundColor.
The same section goes on to say:
In places where you want to perform more sophisticated animations, or animations not supported by the UIView class, you can use Core Animation and the view’s underlying layer to create the animation. Because view and layer objects are intricately linked together, changes to a view’s layer affect the view itself.
A few lines down you can read the list of Core Animation animatable properties where you see this one:
The layer’s border (including whether the layer’s corners are rounded)
There are at least two good options for achieving the effect that you are after:
Animating the corner radius
Using a CAShapeLayer and animating the path
Both of these require that you do the animations with Core Animation. You can create a CAAnimationGroup and add an array of animations to it if you need multiple animations to run as one.
Update:
Fixing things with as few code changes as possible would be done by doing the corner radius animation on the layer at the "same time" as the other animations. I put quotations marks around same time since it is not guaranteed that animations that are not in the same group will finish at exactly the same time. Depending on what other animations you are doing it might be better to use only basic animations and animations groups. If you are applying changes to many different views in the same view animation block then maybe you could look into CATransactions.
The below code animates the frame and corner radius much like you describe.
UIView *circle = [[UIView alloc] initWithFrame:CGRectMake(30, 30, 100, 100)];
[[circle layer] setCornerRadius:50];
[[circle layer] setBorderColor:[[UIColor orangeColor] CGColor]];
[[circle layer] setBorderWidth:2.0];
[[circle layer] setBackgroundColor:[[[UIColor orangeColor] colorWithAlphaComponent:0.5] CGColor]];
[[self view] addSubview:circle];
CGFloat animationDuration = 4.0; // Your duration
CGFloat animationDelay = 3.0; // Your delay (if any)
CABasicAnimation *cornerRadiusAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
[cornerRadiusAnimation setFromValue:[NSNumber numberWithFloat:50.0]]; // The current value
[cornerRadiusAnimation setToValue:[NSNumber numberWithFloat:10.0]]; // The new value
[cornerRadiusAnimation setDuration:animationDuration];
[cornerRadiusAnimation setBeginTime:CACurrentMediaTime() + animationDelay];
// If your UIView animation uses a timing funcition then your basic animation needs the same one
[cornerRadiusAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
// This will keep make the animation look as the "from" and "to" values before and after the animation
[cornerRadiusAnimation setFillMode:kCAFillModeBoth];
[[circle layer] addAnimation:cornerRadiusAnimation forKey:#"keepAsCircle"];
[[circle layer] setCornerRadius:10.0]; // Core Animation doesn't change the real value so we have to.
[UIView animateWithDuration:animationDuration
delay:animationDelay
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
[[circle layer] setFrame:CGRectMake(50, 50, 20, 20)]; // Arbitrary frame ...
// You other UIView animations in here...
} completion:^(BOOL finished) {
// Maybe you have your completion in here...
}];
With many thanks to David, this is the solution I found. In the end what turned out to be the key to it was using the view's -[actionForLayer:forKey:] method, since that's used inside UIView blocks instead of whatever the layer's -[actionForKey] returns.
#implementation SGBRoundView
-(CGFloat)radiusForBounds:(CGRect)bounds
{
return fminf(bounds.size.width, bounds.size.height) / 2;
}
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.opaque = NO;
self.layer.backgroundColor = [[UIColor purpleColor] CGColor];
self.layer.borderColor = [[UIColor greenColor] CGColor];
self.layer.borderWidth = 3;
self.layer.cornerRadius = [self radiusForBounds:self.bounds];
}
return self;
}
-(void)setBounds:(CGRect)bounds
{
self.layer.cornerRadius = [self radiusForBounds:bounds];
[super setBounds:bounds];
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
id<CAAction> action = [super actionForLayer:layer forKey:event];
if ([event isEqualToString:#"cornerRadius"])
{
CABasicAnimation *boundsAction = (CABasicAnimation *)[self actionForLayer:layer forKey:#"bounds"];
if ([boundsAction isKindOfClass:[CABasicAnimation class]] && [boundsAction.fromValue isKindOfClass:[NSValue class]])
{
CABasicAnimation *cornerRadiusAction = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
cornerRadiusAction.delegate = boundsAction.delegate;
cornerRadiusAction.duration = boundsAction.duration;
cornerRadiusAction.fillMode = boundsAction.fillMode;
cornerRadiusAction.timingFunction = boundsAction.timingFunction;
CGRect fromBounds = [(NSValue *)boundsAction.fromValue CGRectValue];
CGFloat fromRadius = [self radiusForBounds:fromBounds];
cornerRadiusAction.fromValue = [NSNumber numberWithFloat:fromRadius];
return cornerRadiusAction;
}
}
return action;
}
#end
By using the action that the view provides for the bounds, I was able to get the right duration, fill mode and timing function, and most importantly delegate - without that, the completion block of UIView animations didn't run.
The radius animation follows that of the bounds in almost all circumstances - there are a few edge cases that I'm trying to iron out, but it's basically there. It's also worth mentioning that the pinch gestures are still sometimes jerky - I guess even the accelerated drawing is still costly.
Starting in iOS 11, UIKit animates cornerRadius if you change it inside an animation block.
The path property of a CAShapeLayer isn't implicitly animatable, but it is animatable. It should be pretty easy to create a CABasicAnimation that changes the size of the circle path. Just makes sure that the path has the same number of control points (e.g. changing the radius of a full-circle arc.) If you change the number of control points, things get really strange. "Results are undefined", according to the documentaiton.
I actually stuck on a problem with animating a UILabel in my iOS Application.
After 2 days of searching the web for code snippets, still no result.
Every sample I found was about how to animate UIImage, adding it as a subview to UIView by layer. Is there any good example about animating a UILabel?
I found a nice solution for a blinking animation by setting the alpha property, like this:
My function:
- (void)blinkAnimation:(NSString *)animationID finished:(BOOL)finished target:(UIView *)target
{
NSString *selectedSpeed = [[NSUserDefaults standardUserDefaults] stringForKey:#"EffectSpeed"];
float speedFloat = (1.00 - [selectedSpeed floatValue]);
[UIView beginAnimations:animationID context:target];
[UIView setAnimationDuration:speedFloat];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(blinkAnimation:finished:target:)];
if([target alpha] == 1.0f)
[target setAlpha:0.0f];
else
[target setAlpha:1.0f];
[UIView commitAnimations];
}
Call my function on the UILabel:
[self blinkAnimation:#"blinkAnimation" finished:YES target:labelView];
But how about a Pulse, or scaling animation?
Unfortunately font size is not an animatable property of NSView. In order to scale a UILabel, you'll need to use more advanced Core Animation techniques, using CAKeyframeAnimation:
Import the QuartzCore.framework into your project, and #import <QuartzCore/QuartzCore.h> in your code.
Create a new CAKeyframeAnimation object that you can add your key frames to.
Create a CATransform3D value defining the scaling operation (don't get confused by the 3D part--you use this object to do any transformations on a layer).
Make the transformation one of the keyframes in the animation by adding it to the CAKeyframeAnimation object using its setValues method.
Set a duration for the animation by calling its setDuration method
Finally, add the animation to the label's layer using [[yourLabelObject layer] addAnimation:yourCAKeyframeAnimationObject forKey:#"anyArbitraryString"]
The final code could look something like this:
// Create the keyframe animation object
CAKeyframeAnimation *scaleAnimation =
[CAKeyframeAnimation animationWithKeyPath:#"transform"];
// Set the animation's delegate to self so that we can add callbacks if we want
scaleAnimation.delegate = self;
// Create the transform; we'll scale x and y by 1.5, leaving z alone
// since this is a 2D animation.
CATransform3D transform = CATransform3DMakeScale(1.5, 1.5, 1); // Scale in x and y
// Add the keyframes. Note we have to start and end with CATransformIdentity,
// so that the label starts from and returns to its non-transformed state.
[scaleAnimation setValues:[NSArray arrayWithObjects:
[NSValue valueWithCATransform3D:CATransform3DIdentity],
[NSValue valueWithCATransform3D:transform],
[NSValue valueWithCATransform3D:CATransform3DIdentity],
nil]];
// set the duration of the animation
[scaleAnimation setDuration: .5];
// animate your label layer = rock and roll!
[[self.label layer] addAnimation:scaleAnimation forKey:#"scaleText"];
I'll leave the repeating "pulse" animation as an exercise for you: hint, it involves the animationDidStop method!
One other note--the full list of CALayer animatable properties (of which "transform" is one) can be found here. Happy tweening!