CAlayer transform on sublayers flickers with gestures (ipad) - ipad

I have a CALayer that contains few other subLayers (CATextLayer actually)
I want to apply some transformation on that layer when user do usual gesture on the ipad but it doesn't seem to be working properly. The goal of using the CALayer was to apply the transformation only to that Layer so that all of my sub-textLayer would be affected at the same time with the same transformation.
What's happening is that the transformation seem to flicker between previous and current position. I don't really get what could be the problem... When I do a 2 fingers panning gesture for example, CaTextLayer positions flickers all the time during my gesture and at the end they are all correctly placed at their new translated position.
So everything seems to work fine except that flickering thing that is bothering me a lot.
Do I need to set some property I don't know about ? I'm thinking it might have something to do with bounds and frame too....
Here's how I create my CATextLayer (This is done only once at creation and it works properly):
_textString = [[NSString alloc] initWithString: text];
_position = position;
attributedTextLayer_ = [[CATextLayer alloc] init];
attributedTextLayer_.bounds = frameSize;
//.. Set the font
attributedTextLayer_.string = attrString;
attributedTextLayer_.wrapped = YES;
CFRange fitRange;
CGRect textDisplayRect = CGRectInset(attributedTextLayer_.bounds, 10.f, 10.f);
CGSize recommendedSize = [self suggestSizeAndFitRange:&fitRange
forAttributedString:attrString
usingSize:textDisplayRect.size];
[attributedTextLayer_ setValue:[NSValue valueWithCGSize:recommendedSize] forKeyPath:#"bounds.size"];
attributedTextLayer_.position = _position;
This is how I add them to my Super CALayer
[_layerMgr addSublayer:t.attributedTextLayer_];
[[_drawDelegate UI_GetViewController].view.layer addSublayer:_layerMgr];
And here's how I apply my transformation :
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);

After lots of reading and testing...I've found my own solution.
It looks like the CoreAnimation uses default animation when you do transformation or operation on any layers. It's very recommended that when you do such CALayer operation that you go through what they call "Transactions".
I've found all about that in the CoreAnimation Programming guide, under the section : transactions.
My solution was then to implement such a transaction, and preventing any animation while doing CALayer operation.
This is what I do when applying my transformation (which prevents flickering) :
-(void)applyTransform
{
if (! CGAffineTransformIsIdentity(_transform))
{
[CATransaction begin];
//This is what prevents all animation during the transaction
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);
[CATransaction commit];
}
}

Related

Is it possible to update the position of a button after a CAKeyFrameAnimation?

I have been trying to update the position of button(s) after the completion of a CAKeyFrameAnimation. As of now, I have stopped the animation at a certain angle on the arc (final angle) but, it seems the touches the buttons take is at the point it was earlier in before the animation takes place.
Here's the code that I have been using:
for(int i = 0; i <numberOfButtons; i++)
{
double angle = [[self.buttonsArray objectAtIndex:i] angle];
if(angle != -50 && angle !=270){
CGPoint arcStart = CGPointMake([[self.buttonsArray objectAtIndex:i] buttonForCricle].center.x, [[self.buttonsArray objectAtIndex:i] buttonForCricle].center.y);
CGPoint arcCenter = CGPointMake(centerPoint.x, centerPoint.y);
CGMutablePathRef arcPath = CGPathCreateMutable();
CGPathMoveToPoint(arcPath, NULL, arcStart.x, arcStart.y);
CGPathAddArc(arcPath, NULL, arcCenter.x, arcCenter.y, 150, ([[self.buttonsArray objectAtIndex:i] angle]*M_PI/180), (-50*M_PI/180), YES);
UIButton *moveButton = (UIButton *)[self.view viewWithTag:i+1];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
// pathAnimation.values = #[#0];
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 1.0;
pathAnimation.path = arcPath;
CGPathRelease(arcPath);
// UIView* drawArcView = nil;
// drawArcView = [[UIView alloc] initWithFrame: self.view.bounds];
// CAShapeLayer* showArcLayer = [[CAShapeLayer alloc] init];
// showArcLayer.frame = drawArcView.layer.bounds;
// showArcLayer.path = arcPath;
// showArcLayer.strokeColor = [[UIColor blackColor] CGColor];
// showArcLayer.fillColor = nil;
// showArcLayer.lineWidth = 1.0;
// [drawArcView.layer addSublayer: showArcLayer];
// [self.view addSubview:drawArcView];
[moveButton.layer addAnimation:pathAnimation forKey:#"arc"];
[CATransaction commit];
[self.view bringSubviewToFront:moveButton];
}
}
How can I update the buttons position after this?
Why this is happening
What you're seeing is that animations only change the "presentation values" of a layer, without changing the "model values". Add to this the fact that animations by default are removed when they complete and you have an explanation for why the button(s) would return to their original position after the animation finishes.
This is the state of the actual button, and the location where it is hit tested when the user taps on it.
The combination of removedOnCompletion = NO and fillMode = kCAFillModeForwards makes it look like the button has moved to it's final position, but it's actually causing the difference between presentation values and model values to persists.
Put simply, the button looks like it's in one position, but it's actually in another position.
How to fix it
For this type of situation, I recommend to let the animation be removed when it finishes, and to not specify any special fill mode, and then solve the problem of the button(s) position being incorrect.
They way to do this is to set the buttons position/center to its final value. This will make it so that the button's model position changes immediately, but during the animation its presentation position remains the same. After the animation finishes and is removed the button goes back to its model value, which is the same as the final value of the animation. So the button will both look and be in the correct position.
Usually the easiest place to update the model value is after having created the animation object but before adding it to the layer.
Changing the positions will create and add an implicit animation. But as long as the explicit animation (the one you're creating) is longer, the implicit animation won't be visible.
If you don't want this implicit animation to be created, then you can create a CATransansaction around only the line where the position property is updated, and disable actions (see setDisableActions:).
If you want to learn more, I have a blog post about how layers work with multiple animations and an answer to a question about when to update the layer.

CABasicAnimation to path strange behaviour [duplicate]

I am running into an issue when I create an explicit animation to change the value of a CAShapeLayer's path from an ellipse to a rect.
In my canvas controller I setup a basic CAShapeLayer and add it to the root view's layer:
CAShapeLayer *aLayer;
aLayer = [CAShapeLayer layer];
aLayer.frame = CGRectMake(100, 100, 100, 100);
aLayer.path = CGPathCreateWithEllipseInRect(aLayer.frame, nil);
aLayer.lineWidth = 10.0f;
aLayer.strokeColor = [UIColor blackColor].CGColor;
aLayer.fillColor = [UIColor clearColor].CGColor;
[self.view.layer addSublayer:aLayer];
Then, when I animate the path I get a strange glitch / flicker in the last few frames of the animation when the shape becomes a rect, and in the first few frames when it animates away from being a rect. The animation is set up as follows:
CGPathRef newPath = CGPathCreateWithRect(aLayer.frame, nil);
[CATransaction lock];
[CATransaction begin];
[CATransaction setAnimationDuration:5.0f];
CABasicAnimation *ba = [CABasicAnimation animationWithKeyPath:#"path"];
ba.autoreverses = YES;
ba.fillMode = kCAFillModeForwards;
ba.repeatCount = HUGE_VALF;
ba.fromValue = (id)aLayer.path;
ba.toValue = (__bridge id)newPath;
[aLayer addAnimation:ba forKey:#"animatePath"];
[CATransaction commit];
[CATransaction unlock];
I have tried many different things like locking / unlocking the CATransaction, playing with various fill modes, etc...
Here's an image of the glitch:
http://www.postfl.com/outgoing/renderingglitch.png
A video of what I am experiencing can be found here:
http://vimeo.com/37720876
I received this feedback from the quartz-dev list:
David Duncan wrote:
Animating the path of a shape layer is only guaranteed to work when
you are animating from like to like. A rectangle is a sequence of
lines, while an ellipse is a sequence of arcs (you can see the
sequence generated by using CGPathApply), and as such the animation
between them isn't guaranteed to look very good, or work well at all.
To do this, you basically have to create an analog of a rectangle by
using the same curves that you would use to create an ellipse, but
with parameters that would cause the rendering to look like a
rectangle. This shouldn't be too difficult (and again, you can use
what you get from CGPathApply on the path created with
CGPathAddEllipseInRect as a guide), but will likely require some
tweaking to get right.
Unfortunately this is a limitation of the otherwise awesome animatable path property of CAShapeLayers.
Basically it tries to interpolate between the two paths. It hits trouble when the destination path and start path have a different number of control points - and curves and straight edges will have this problem.
You can try to minimise the effect by drawing your ellipse as 4 curves instead of a single ellipse, but it still isn't quite right. I haven't found a way to go smoothly from curves to polygons.
You may be able to get most of the way there, then transfer to a fade animation for the last part - this won't look as nice, though.

How To Apply Complex CALayer Transform Changes?

I need a complex continuous animation of a UIView that involves setting CATransform3D rotation and translation properties that need to be calculated, so a standard animation is no option.
I turned to using CALayer animation. And have this:
self.displayLink = [self.window.screen displayLinkWithTarget:self selector:#selector(update:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
- (void)update:(CADisplayLink*)sender
{
CGFloat elapsedTime = sender.timestamp - self.lastTimestamp;
self.lastTimestamp = sender.timestamp;
self.rotation += elapsedTime * 0.1; // Factor determines speed.
// This is just example for SO post; the real animation is more complicated!
CATransform3D transform;
transform = CATransform3DMakeRotation(self.rotation, 1.0, 0.0, 0.0);
self.imageLayer.transform = transform;
}
Here self.imageLayer is a CALayer whose contents has been set with an image and added as a sublayer to my base-view.
It does rotate 'a bit' but not continuously, sometimes it seems to stop or rotate backwards a bit.
It seems that assigning a new transform quite often does not have any effect because the self.rotation value is incremented much much more. Adding a [self setNeedsDisplay] did not help.
I've not done much with CALayers yet so I guess that I'm missing something very basic. Tried to find it for some time but all examples I found seem too far from what I want.
When you create your own layer and then modify its properties, it animates them implicitly (see “Animating Simple Changes to a Layer’s Properties”. You're not seeing the changes you expect because Core Animation is animating the changes rather than applying them instantly.
You need to disable implicit animation. There are several ways to disable it; see “Changing a Layer’s Default Behavior”. Or search stack overflow for “disable implicit animations”. Here's one way:
NSDictionary *actions = #{ #"transform": [NSNull null] };
self.imageLayer.actions = actions;
Just do that once, where you create imageLayer.

The movement of view with applied transformation on iPad Air (with retina display)

If I apply transformation for a view, then movement of the view on iPad Air happens with lags. It looks like implicit animations in CALayer.
I've created test project. It should be executed on iPad simulator.
This is code that I use for apply transformations:
CGAffineTransform transform = CGAffineTransformIdentity;
if (_transformSwitch.on)
{
transform = CGAffineTransformRotate(CGAffineTransformMakeScale(2.0, 2.0), M_PI / 3.0);
}
self.frameView.transform = transform;
This is code that I use for apply movement:
CGPoint transition = [_panRecognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(_startCenter.x + transition.x, _startCenter.y + transition.y);
self.frameView.center = newCenter;
How I can move the view with applied transformations and avoid the animations?
UPD
I've found a solution for a moving by timer invocation, but if I'm moving frame by a finger, I've have problem with the animation.
I set a center of the view with wrapping it with [CATransaction begin] [CATransaction commit]:
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
self.frameView.center = center;
[CATransaction commit];
I've found strange solution, but I want to know why is it a worked solution?
I added to this method setNeedsDisplay, and it's solved my problem. (If I add any view over my screen with drawRect method, and call setNeedsDisplay on it view, it's also solved my problem):
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
[self.frameView setNeedsDisplay];
self.frameView.center = center;
[CATransaction commit];
I've updated my project.
UPD2
This lag happens only on iPad with retina display (for example iPad Air). I've created small video to illustrate the problem: https://yadi.sk/i/rAneCEFmjjEej
I think you could wrap the changes in this code:
[CATransaction begin];
[CATransaction disableActions];
// Your changes to the UI elements...
[CATransaction commit];
Alright, I've played around with your project and found 2 things that do not what you expected they would do:
The 1st one is drawRect: on FrameView:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
From Apple:
The default implementation of this method does nothing. Subclasses
that use technologies such as Core Graphics and UIKit to draw their
view’s content should override this method and implement their drawing
code there. You do not need to override this method if your view sets
its content in other ways. For example, you do not need to override
this method if your view just displays a background color or if your
view sets its content directly using the underlying layer object.
The 2nd one is the wrong (it's right according to the doc, see below) return value of the overridden - (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event on FrameView: you return nil (as described in the CALayerDelegate reference) while CoreAnimation, in fact, expects an instance of NSNull (as might be assumed when reading CALayer class reference). Your actions led to an unintended result as there's a collision in the documentation. I'm talking about this and this.
P.S. As a result, I made it working without using CATransaction. Unfortunately, I don't have a retina iPad by hand, so please check out my changes to your GitHub project (I will create a pull request containing all my changes) and tell if it resolves both the unintended animation (assuming resolved) problem and the lag of dragging (need to check for performance issues on a real iPad).

Can't get a CALayer to update its drawLayer: DURING a bounds animation

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

Resources