I have a UIView which has several subviews that respond to touch. If I rotate the layer of the superview with some code like this:
[CATransaction begin];
[CATransaction setDisableActions:true];
view.layer.transform = CATransform3DRotate(view.layer.transform, angle, 0, 0, 1);
[CATransaction commit];
... then I can't seem to find the right location of the subviews when responding to touch events... Any ideas?
I found it, worked out beautifully I guess. I needed to convert points between UIViews for doing bezier hit testing:
-(BOOL)containsPoint:(CGPoint)point
{
point = [self.superview.superview convertPoint:point toView:self];
return [self.slicePath containsPoint:point];
}
Related
I have an AVCaptureVideoPreviewLayer on screen. I want to change its position instantly after the user touches a button.
I am able to change its position using the following code, but it is not happening instantly but with some kind of smooth liner transition.
CGRect frame = self.preview.frame;
frame.origin.x = previewLeft;
frame.origin.y = previewTop;
self.preview.frame = frame;
How can I make it to change its position without any kind of intrinsic animation?
CALayer.frame is a computed property, but it cannot be explicitly animated. By setting it, you're also setting position, which is implicitly animated.
You can temporarily suppress all actions, the mechanism by which Core Animation creates its default layer animations:
Swift code:
CATransaction.begin()
CATransaction.setDisableActions(true)
self.preview.frame = frame;
CATransaction.commit()
Objective-C code:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.preview.frame = frame;
[CATransaction commit];
LucasTizma's answer is perfect.
Besides, there is a trick to solve this issue.
Just slow down the window speed:
[(CALayer *)[[[[UIApplication sharedApplication] windows] objectAtIndex:0] layer] setSpeed:0.9];
Don't forget to correct it to 1.0f after your transition.
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).
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 use pan gesture to move an image in CALayer. The issue I experience is that the image appears to move with a little delay and does not appear 'stuck' to my finger.
Here is the actual snippet of how I move the layer(facePic is the CALayer):
CGPoint translation =[touche locationInView:self.view];
self.facePic.frame =
CGRectMake(translation.x - self.facePic.frame.size.width/2,
translation.y - self.facePic.frame.size.height/2,
self.facePic.frame.size.width,
self.facePic.frame.size.height);
I think you see the result of implicit animation of a layer. If so then there are two options to disable this animation:
use transactions
set layer actions
To use transactions wrap your code with CATransaction
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
. . .
[CATransaction commit];
To disable some layer actions you can add this to the layer init, the position animation for example:
aLayer.actions = #{#"position":[NSNull null]}; // FIXED property name