Tweening / Interpolating between two CGPaths / UIBeziers
This is actually a lot simpler then you would first think and uses animations with "no" speed (paused). Now with the paused animation added to the layer you can change the time offset to jump to a specific time within the animation.
If you are not planning on running the animation by itself (i.e. only control it manually) I would suggest that you change the duration of the animation to 1. This means that you change the time offset from 0 to 1 when moving from 0% to 100%. If your duration was 0.3 (as in your example) then you would set the time offset to a value between 0 and 0.3 instead.
Implementation
As I said above, there is very little code involved in this little trick.
- (Optional) Set the duration to 1.0
- Set the
speed
of the layer (yes they conform toCAMediaTiming
) or the animation to 0.0 - During the drag gesture or slider (as in my example) set the
timeOffset
of the layer or animation (depending on what you did in step 2).
This is how I configured my animation + shape layer
CABasicAnimation *morph = [CABasicAnimation animationWithKeyPath:@"path"];
morph.duration = 1.; // Step 1
morph.fromValue = (__bridge id)fromPath;
morph.toValue = (__bridge id)toPath;
[self.shapeLayer addAnimation:morph forKey:@"morph shape back and forth"];
self.shapeLayer.speed = 0.0; // Step 2
And the slider action:
- (IBAction)sliderChanged:(UISlider *)sender {
self.shapeLayer.timeOffset = sender.value; // Step 3
}
You can see the end result below. Happy coding!
Interpolating CGAffineTransform
If you want to be able to interact with the animation (maybe using a gesture or a slider or some other mechanism) then you can use a little trick where you "pause" the animation by setting the speed
of the view's layer to 0 and then move the animation to a specific point in time by setting the timeOffset
of the layer.
I have an explanation of this in an answer to a similar (but animating a different property) question and I a more detailed explanation in the context of animation timing in a blog post.
So that this isn't just a link answer, this is the very little code that you would need to do this interaction. I'm assuming a slider in this case. The blog post shows how to use it with scroll events and if you want to do it with gestures then I'm sure you can figure it out :)
In you animation setup (note that I'm using the CATransform3D instead of CGAffineTransform)
CABasicAnimation *yourAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
yourAnimation.duration = 1.; // For convenience (so that timeOffset is from 0.0 to 1.0)
yourAnimation.fromValue = [NSValue valueWithCATransform3D:fromTransform];
yourAnimation.toValue = [NSValue valueWithCATransform3D:toTransform];
[self.yourView.layer addAnimation: yourAnimation forKey:@"Your Animation"];
self.yourView.layer.speed = 0.0; // Pause every animation on that layer
And the slider to drive the interaction of the animation (slider is configured to go from 0.0 to 1.0):
- (IBAction)sliderChanged:(UISlider *)sender {
self.yourView.layer.timeOffset = sender.value; // Change the "current time" of the animation
}
Filling colour between two concentric ovals
If you are just trying to fill the smaller oval with a white colour, change your fillColor2 code to this:
UIColor *fillColor2 = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
or this:
UIColor *fillColor2 = [UIColor whiteColor];
The smaller circle is showing up black because filling red, green, and blue values with 0 results in black.
iOS: CAShape Layer Path Transformation
By carefully choosing your control points, etc, you can probably cook up a path which draws as a triangle, but which has the same number of segments and control points as the circle you want to draw. Like this (all numeric values assume iPhone screen, but the point is general):
@implementation ViewController
{
CAShapeLayer* shapeLayer;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
shapeLayer = [[CAShapeLayer alloc] init];
CGRect bounds = self.view.bounds;
bounds.origin.x += 0.25 * bounds.size.width;
bounds.size.width *= 0.5;
bounds.origin.y += 0.25 * bounds.size.height;
bounds.size.height *= 0.5;
shapeLayer.frame = bounds;
shapeLayer.backgroundColor = [[UIColor redColor] CGColor];
[self.view.layer addSublayer: shapeLayer];
[self toCircle: nil];
}
CGPoint AveragePoints(CGPoint a, CGPoint b)
{
return CGPointMake((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
}
- (IBAction)toCircle:(id)sender
{
UIBezierPath* p = [[UIBezierPath alloc] init];
[p moveToPoint: CGPointMake(80, 56)];
[p addCurveToPoint:CGPointMake(144, 120) controlPoint1:CGPointMake(115.34622, 56) controlPoint2:CGPointMake(144, 84.653778)];
[p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:CGPointMake(144, 131.23434) controlPoint2:CGPointMake(141.0428, 142.27077)];
[p addCurveToPoint:CGPointMake(48, 175.42563) controlPoint1:CGPointMake(117.75252, 182.61073) controlPoint2:CGPointMake(78.610725, 193.09874)];
[p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:CGPointMake(38.270771, 169.80846) controlPoint2:CGPointMake(30.191547, 161.72923)];
[p addCurveToPoint:CGPointMake(47.999996, 64.574379) controlPoint1:CGPointMake(6.9012618, 121.38927) controlPoint2:CGPointMake(17.389269, 82.24749)];
[p addCurveToPoint:CGPointMake(80, 56) controlPoint1:CGPointMake(57.729225, 58.957207) controlPoint2:CGPointMake(68.765656, 56)];
[p closePath];
[CATransaction begin];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.duration = 3.f;
pathAnimation.fromValue = (id)shapeLayer.path;
pathAnimation.toValue = (id)p.CGPath;
[shapeLayer addAnimation:pathAnimation forKey:@"path"];
[CATransaction setCompletionBlock:^{
shapeLayer.path = p.CGPath;
}];
[CATransaction commit];
double delayInSeconds = 4.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self toTriangle: nil];
});
}
- (IBAction)toTriangle: (id)sender
{
UIBezierPath* p = [[UIBezierPath alloc] init];
// Triangle using the same number and kind of points...
[p moveToPoint: CGPointMake(80, 56)];
[p addCurveToPoint: AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152)) controlPoint1:CGPointMake(80, 56) controlPoint2:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152))];
[p addCurveToPoint:CGPointMake(135.42563, 152) controlPoint1:AveragePoints(CGPointMake(80, 56), CGPointMake(135.42563, 152)) controlPoint2:CGPointMake(135.42563, 152)];
[p addCurveToPoint:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint1:CGPointMake(135.42563, 152) controlPoint2:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152))];
[p addCurveToPoint:CGPointMake(24.574375, 152) controlPoint1:AveragePoints(CGPointMake(135.42563, 152), CGPointMake(24.574375, 152)) controlPoint2:CGPointMake(24.574375, 152)];
[p addCurveToPoint: AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint1:CGPointMake(24.574375, 152) controlPoint2:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) ];
[p addCurveToPoint:CGPointMake(80, 56) controlPoint1:AveragePoints(CGPointMake(24.574375, 152),CGPointMake(80, 56)) controlPoint2:CGPointMake(80, 56)];
[p closePath];
[CATransaction begin];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
pathAnimation.duration = 3.f;
pathAnimation.fromValue = (id)shapeLayer.path;
pathAnimation.toValue = (id)p.CGPath;
[shapeLayer addAnimation:pathAnimation forKey:@"path"];
[CATransaction setCompletionBlock:^{
shapeLayer.path = p.CGPath;
}];
[CATransaction commit];
double delayInSeconds = 4.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self toCircle: nil];
});
}
Even with the number of segments and control points being the same, the animation still looks a little wonky and probably isn't what you wanted. The reason for that is that CA seems to be matching up all the segments and control points one-to-one and then linearly interpolating between them for all points. Because the relationship between control points and the resulting path is not linear (cubic in this case), interpolating control point positions linearly isn't going to result in the path moving in a linear fashion. If you run this code, you can see that as it transitions, there are weird humps in the side of the triangle where the circle path had other points in the arc.
More generally, it's just not going to be reasonable to expect CA to magically morph between two arbitrary paths in some specific way that turns out to be desirable in appearance. Even going to the effort to hand construct these paths such that they might have a prayer of morphing, they still failed to look like I thought they should.
It might be more reasonable to achieve the desired effects by working with flattened paths (i.e. paths made up of many small straight lines instead of curved path elements). Even that seems like it would be non-trivial, since you would, again, need both paths to have the same number of segments, and you would have to construct those segments such that the common points were the right number of segments along the whole path.
In sum: This is a fairly complex problem and the naive/free solution that CoreAnimation has provided is unlikely to what you want.
Ios drawing lines animated
If I've understood what you are trying to do, then I see no reason why you shouldn't be able to do that using Core Animation.
If the pattern you are moving is as simple as that then you can use a line dash pattern on a CAShapeLayer to draw the pattern. I created my layer so that it gets the line width from the height of the bounds and the shape layer's path gets its start and end points from the bounds of the layer:
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.bounds = CGRectMake(0, 0, 200, 60);
lineLayer.position = self.view.center;
lineLayer.strokeColor = [UIColor blackColor].CGColor;
lineLayer.lineDashPattern = @[@5, @2, @2, @2, @2, @2, @2, @2, @2, @2];
lineLayer.lineWidth = CGRectGetHeight(lineLayer.bounds);
UIBezierPath *linePath = [UIBezierPath bezierPath];
[linePath moveToPoint:CGPointMake(0, CGRectGetMidY(lineLayer.bounds))];
[linePath addLineToPoint:CGPointMake(CGRectGetMaxX(lineLayer.bounds), CGRectGetMidY(lineLayer.bounds))];
lineLayer.path = linePath.CGPath;
[self.view.layer addSublayer:lineLayer];
That will produce the static drawing of the pattern. The line dash pattern in the code is the length of the alternating segments of where the line is shown and where it is not.
With the path drawn, I did the animation by altering the "phase" of the line dashes (shifting them in one direction or the other). To make it seem like the pattern is smoothly and continuously moving I created a linear, repeating animation that shifts the pattern by its full length:
NSNumber *totalDashLenght = [lineLayer.lineDashPattern valueForKeyPath:@"@sum.self"]; // KVC is awesome :)
CABasicAnimation *animatePhase = [CABasicAnimation animationWithKeyPath:@"lineDashPhase"];
animatePhase.byValue = totalDashLenght; // using byValue means that even if the layer has a shifted phase, it will shift on top of that.
animatePhase.duration = 3.0;
animatePhase.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animatePhase.repeatCount = INFINITY;
[lineLayer addAnimation:animatePhase forKey:@"marching ants"];
The animated line looks like this:
If you want different lines to animate at different speeds you change the duration
of the animation. This value represents the time it takes to animate the shifting one full phase.
Creating Clickable CGPaths
You are already creating paths, you're just creating them in the context rather than creating them and then adding them to the context. You don't need to do that. You can use CGPathCreateMutable
, CGPathAddArc
and CGContextAddPath
.
You can also use UIBezierPath
, either via bezierPathWithCGPath:
or directly (and then get the CGPath
from them).
And, once you have bezier paths, you can call containsPoint:
on them to hit test touches.
Animate image with gesture
Curve Setup:
self.trackPath = [UIBezierPath bezierPath];
self.trackPath = [UIBezierPath bezierPathWithArcCenter:P(self.scrollView.center.x,self.scrollView.center.y-20)
radius:110
startAngle:DEGREES_TO_RADIANS(70)
endAngle:DEGREES_TO_RADIANS(115)
clockwise:NO];
self.plane = [CALayer layer];
self.plane.bounds = CGRectMake(0, 0, 60.0, 60.0);
self.plane.position = CGPointMake(self.scrollView.center.x,self.scrollView.center.y-20);
self.plane.contents = (id)([UIImage imageNamed:@"profile_tag"].CGImage);
[self.view.layer addSublayer:self.plane];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
anim.path = self.trackPath.CGPath;
anim.duration = 320.;
[self.plane addAnimation:anim forKey:nil];
self.plane.speed = 0.0;
self.plane.hidden = YES;
When Paging:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat pageWidth = self.scrollView.frame.size.width;
int page = floor((self.scrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;
self.pageControl.currentPage = page;
if((scrollView.contentOffset.x >= 320) && (scrollView.contentOffset.x<=640)){
self.plane.hidden = NO;
self.plane.timeOffset = scrollView.contentOffset.x - 320;
}
}
Related Topics
Nsmutableurlrequest Timeout Interval Not Taken into Consideration for Post Requests
Add Child View Controller to Current View Controller
Uicollectionview Adding Image to a Cell
How to Add iPhone 5 Large Screen Support to iOS Apps in Xcode
How to Determine Height of Uicollectionview with Flowlayout
Access Container View Controller from Parent iOS
Uitableview Within Uiscrollview Using Autolayout
How to Set iOS Status Bar Background Color in React Native
iOS Aes Encryption - Fail to Encrypt
Swift - Saving Highscore Using Nsuserdefaults
Force Landscape Viewcontroller in iOS 7
Trigger Local Notifications Automatically Daily on Dynamic Time Given in Arrays Objective C iOS
Optional Chaining in Swift Closure Where Return Type Has to Be Void
How to Detect One Button in Tableview Cell
iPhone In-App Purchase Store Kit Error -1003 "Cannot Connect to Itunes Store"