Move a UIImageView - ios

What is the best way to move an image along an array of dots?

My recommended approach would be to wrap the UIImage in a UIImageView and use a CAKeyframeAnimation to animate your UIImageView's layer along a path that passes through your three points:
UIImage *image = [UIImage imageNamed:#"image.png"];
imageView = [[UIImageView alloc] initWithImage:image];
[mainView addSubview:imageView];
// Remember to remove the image view and release it when done with it
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 1.0f;
pathAnimation.calculationMode = kCAAnimationPaced;
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
CGMutablePathRef pointPath = CGPathCreateMutable();
CGPathMoveToPoint(pointPath, NULL, viewOrigin.x, viewOrigin.y);
CGPathAddLineToPoint(pointPath, NULL, point1.x, point1.y);
CGPathAddLineToPoint(pointPath, NULL, point2.x, point2.y);
CGPathAddLineToPoint(pointPath, NULL, point3.x, point3.y);
pathAnimation.path = pointPath;
CGPathRelease(pointPath);
[imageView.layer addAnimation:pathAnimation forKey:#"pathAnimation"];
Note that by default, the position of a layer is at the layer's center. If you'd like to move the layer relative to another reference point, you can set the layer's anchorPoint property to something like (0.0, 0.0) for its upper-left corner (on the iPhone) or (0.0, 1.0) for its lower left.
Also, this won't change the frame of the UIImageView when it's done, so if you refer to that frame later on, you may need to either take that into account or add a delegate method callback for the end of your animation to set it to the proper value.
You can also make your image move along curves, instead of straight lines, by replacing the calls to CGPathAddLineToPoint() with CGPathAddCurveToPoint().
EDIT (5/14/2009): I added the missing pathAnimation.path = pointPath line and changed a mistyped reference to curvedPath to pointPath.

The easiest way is to uses UIView animations
A quick example that assumes you are able to use UIImageView to hold your image and NSArray to hold your point.
UIImage *image = [UIImage imageNamed:#"image.png"];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
[someView addSubview:imageView]; // Assume someView exists
NSValue *firstPoint = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
NSValue *secondPoint = [NSValue valueWithCGPoint:CGPointMake(100, 0)];
NSValue *thirdPoint = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
// And so on....
NSArray *points = [NSArray arrayWithObjects:firstPoint, secondPoint, thirdPoint, nil];
for (NSValue *pointValue in points) {
[UIView beginAnimations:#"UIImage Move" context:NULL];
CGPoint point = [pointValue CGPointValue];
CGSize size = imageView.frame.size;
imageView.frame = CGRectMake(point.x, point.y, size.width, size.height);
[UIView commitAnimations];
}

Related

Why CALayer and CAShapeLayer behaving differently

First I created the CAShapeLayer for masking the UIView at the bottom with height of 10px like this:
CAShapeLayer *mask = [[CAShapeLayer alloc]init];
CGRect maskRect = CGRectMake(0, self.view.frame.size.height-10, self.view.frame.size.width, 10.0);
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
mask.path = path;
CGPathRelease(path);
mask.anchorPoint = CGPointMake(0.5, 1.0);
self.view.layer.mask = mask;
After commenting the above code I created a CALayer for masking the UIView at the bottom with height of 10px like this:
CALayer *mask = [CALayer layer];
mask.contents = (id)[[self getImg]CGImage];
mask.frame = CGRectMake(0, self.view.frame.size.height-5, self.view.frame.size.width, 10.0);
mask.anchorPoint = CGPointMake(0.5, 1.0);
self.view.layer.mask = mask;
Question 1:
To perfectly touch the bottom not a pixel above or below, for CAShapeLayer the CGRect I defined is
CGRectMake(0, self.view.frame.size.height-10, self.view.frame.size.width, 10.0);
and for CALayer the CGRect I defined is
CGRectMake(0, self.view.frame.size.height-5, self.view.frame.size.width, 10.0);
why -10 for CAShapeLayer and why -5 for CALayer ?
The animation code and its effects on CAShapeLayer and CALayer:
CGRect oldBounds = mask.bounds;
CGRect newBounds = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height/2);
[UIView animateWithDuration:1.0 animations:^{
CABasicAnimation* revealAnimation = [CABasicAnimation animationWithKeyPath:#"bounds"];
revealAnimation.fromValue = [NSValue valueWithCGRect:oldBounds];
revealAnimation.toValue = [NSValue valueWithCGRect:newBounds];
revealAnimation.duration = 0.5;
[mask addAnimation:revealAnimation forKey:#"revealAnimation"];
}];
// Update the bounds so the layer doesn't snap back when the animation completes.
mask.bounds = newBounds;
Effect on CAShapeLayer the position is changed but no effect on hight like this:
Effect on CALayer which worked perfectly and the result I wanted:
Question 2:
Why CAShapeLayer changed the position not the height?
I don't know why but I am having a feeling like CALayer animation is not going smooth?
Question 3:
The [UIView animateWithDuration:1.0 animations:^{ ... block do not have any effect on animation, why?
Answer 1:
Not sure why it is 5 and 10 for CAShapeLayer and CALayer, need to debug closely, but maybe problem is in adjusting anchorPoint or because you use path of shape layer
Answer 2:
About effect on CAShapeLayer: this is because you assigned CGPath to it, and changed shape layer frame, but not path. So you need to set new path property with correct coordinates. Something like this:
CGRect maskRect = CGRect newBounds = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height/2);
CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
mask.path = path;
CGPathRelease(path);
Answer 3:
CABasicAnimation is class, which will do all animation, and you don't need to use it in UIView animateWithDuration block.
This code should work as expected:
CABasicAnimation* revealAnimation = [CABasicAnimation animationWithKeyPath:#"bounds"];
revealAnimation.fromValue = [NSValue valueWithCGRect:oldBounds];
revealAnimation.toValue = [NSValue valueWithCGRect:newBounds];
revealAnimation.duration = 0.5;
[mask addAnimation:revealAnimation forKey:#"revealAnimation"];

CABasicAnimation not working on device (iPhone 5s)

I have a CABasicAnimation that creates an iris wipe effect on an image. In short, the animation works fine on the simulator but there is no joy on device. The timers still fire correctly and the animationCompleted block gets called however there is no visible animation.
Here is the code to get the iris wipe working:
- (void)irisWipe
{
animationCompletionBlock theBlock;
_resultsImage.hidden = FALSE;//Show the image view
[_resultsImage setImage:[UIImage imageNamed:#"logoNoBoarder"]];
[_resultsImage setBackgroundColor:[UIColor clearColor]];
[_resultsImage setFrame:_imageView.bounds];
//Create a shape layer that we will use as a mask for the waretoLogoLarge image view
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGFloat maskHeight = _resultsImage.layer.bounds.size.height;
CGFloat maskWidth = _resultsImage.layer.bounds.size.width;
CGPoint centerPoint;
centerPoint = CGPointMake( maskWidth/2, maskHeight/2);
//Make the radius of our arc large enough to reach into the corners of the image view.
CGFloat radius = sqrtf(maskWidth * maskWidth + maskHeight * maskHeight)/2;
// CGFloat radius = MIN(maskWidth, maskHeight)/2;
//Don't fill the path, but stroke it in black.
maskLayer.fillColor = [[UIColor clearColor] CGColor];
maskLayer.strokeColor = [[UIColor blackColor] CGColor];
maskLayer.lineWidth = radius; //Make the line thick enough to completely fill the circle we're drawing
// maskLayer.lineWidth = 10; //Make the line thick enough to completely fill the circle we're drawing
CGMutablePathRef arcPath = CGPathCreateMutable();
//Move to the starting point of the arc so there is no initial line connecting to the arc
CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y-radius/2);
//Create an arc at 1/2 our circle radius, with a line thickess of the full circle radius
CGPathAddArc(arcPath,
nil,
centerPoint.x,
centerPoint.y,
radius/2,
3*M_PI/2,
-M_PI/2,
NO);
maskLayer.path = arcPath;
//Start with an empty mask path (draw 0% of the arc)
maskLayer.strokeEnd = 1.0;
CFRelease(arcPath);
//Install the mask layer into out image view's layer.
_resultsImage.layer.mask = maskLayer;
//Set our mask layer's frame to the parent layer's bounds.
_resultsImage.layer.mask.frame = _resultsImage.layer.bounds;
//Create an animation that increases the stroke length to 1, then reverses it back to zero.
CABasicAnimation *swipe = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
swipe.duration = 1;
swipe.delegate = self;
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
swipe.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
swipe.fillMode = kCAFillModeForwards;
swipe.removedOnCompletion = NO;
swipe.autoreverses = NO;
swipe.toValue = [NSNumber numberWithFloat: 0];
//Set up a completion block that will be called once the animation is completed.
theBlock = ^void(void)
{
NSLog(#"completed");
};
[swipe setValue: theBlock forKey: kAnimationCompletionBlock];
// doingMaskAnimation = TRUE;
[maskLayer addAnimation: swipe forKey: #"strokeEnd"];
}
Is there something in iOS7 I should be aware of when working with CAAnimations etc? OR is there a error in the code?
Note this code was sourced from: How do you achieve a "clock wipe"/ radial wipe effect in iOS?
I think the problem (or at least part of it) may be this line:
[_resultsImage setFrame:_imageView.bounds];
That should read
[_resultsImage setBounds:_imageView.bounds];
Instead. If you set the FRAME to the bounds of the image view, you're going to move the image view to 0.0 in its superview.
I'm also not clear what _imageView is, as opposed to _resultsImage.
I would step through your code in the debugger, looking at the frame rectangles that are being set for the image view and mask layer, and all the other values that are calculated.

Change position of UIImageView based on UIBezierPath

I am developing a circular progress bar using UIBezierPath. The progress bar changes its color and position based on a randomly generated float number.
So far the progress bar works fine. I used the following code in order to draw and animate the color:
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor colorWithRed:244.0/255.0 green:244.0/255.0 blue:244.0/255.0 alpha:1.0];
loader = [[UIImageView alloc]initWithFrame:CGRectMake(39, 110, 240, 130)];
loader.image = [UIImage imageNamed:#"loader.png"];
[self.view bringSubviewToFront:loader];
[self.view addSubview:loader];
// Draw the arc with bezier path
int radius = 100;
arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.lineCap = kCALineCapRound;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
// Gradient of progress bar
gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.view.frame;
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor ];
gradientLayer.startPoint = CGPointMake(0,0.1);
gradientLayer.endPoint = CGPointMake(0.5,0.2);
[self.view.layer addSublayer:gradientLayer];
gradientLayer.mask = arc;
refreshButton = [[UIButton alloc] initWithFrame:CGRectMake(50, 370, 50, 50)];
[refreshButton addTarget:self action:#selector(refresh:) forControlEvents:UIControlEventTouchUpInside];
[refreshButton setBackgroundImage:[UIImage imageNamed:#"refresh.png"] forState:UIControlStateNormal];
[self.view addSubview:refreshButton];
smallButton = [[UIImageView alloc] initWithFrame:CGRectMake(285, 225, 15, 15)];
smallButton.image = [UIImage imageNamed:#"buttonSmall.png"];
[self.view addSubview:smallButton];
}
-(void)refresh:(id)sender{
NSLog(#"Refresh action:");
CGFloat randomValue = ( arc4random() % 256 / 256.0 );
NSLog(#"%f", randomValue);
valueLabel.text = #"";
valueLabel = [[UILabel alloc] initWithFrame:CGRectMake(150, 370, 100, 50)];
valueLabel.text = [NSString stringWithFormat: #"%.6f", randomValue];
[self.view addSubview:valueLabel];
// Animation of the progress bar
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
//drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
//drawAnimation.fillMode = kCAFillModeRemoved;
drawAnimation.fillMode = kCAFillModeForwards;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:randomValue];
drawAnimation.toValue = [NSNumber numberWithFloat:randomValue];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
if (randomValue >= 0.500000) {
gradientLayer.colors = #[(__bridge id)[UIColor purpleColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor ];
} else if (randomValue <= 0.089844){
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor redColor].CGColor ];
}
else{
gradientLayer.colors = #[(__bridge id)[UIColor redColor].CGColor,(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor purpleColor].CGColor ];
}
}
Now I want to change the position of the small button on the right side of the progress bar (see first image with the red arrow) based on the current position of the progress bar. I tried to change its frame and setting the x and y position of the image to randomValue but all i got was the image drawn at a random place on the screen.
Does anyone have an idea how to tackle this?
Any help would be greatly appreciated.
Thank you very much!
Granit
There are various ways to tackle this.
The simplest: Since your Bezier curve is a half-circle, use trig to calculate the position along your circle.
I noticed that you are animating your colored bar using strokeEnd values from 0 to 10. The valid values of strokeEnd range from 0.0 to 1.0. You should change that to animate from 0.0 to 1.0.
Once you've fixed that, you code is animating an arc from 0 to 180 degrees. (Actually, you're going backwards, from 180 degrees to 0 degrees, and trig in iOS is based on Radians, which range from 0 to 2π.
If you have an input number value, which ranges from 0 to 1, a CGPoint center that is the center of your arc, and a value r which is distance from the center of your button to the center of the circle (radius of the circle your button will travel along) then your point position would be:
angle = 2* M_PI * (1-value);
buttonCenter.x = center.x + r * cos(angle);
buttonCenter.y = center.y + r * sin(angle);
Another option that would let you animate it would be to create a transform that rotates the button around your center point. You could even use UIView animation to do this. (To build your transform: start with identity transform. Translate center point, rotate, translate back.)
The other options all involve using a bezier path or CGPath. You would need to create a path that mirrors the path of your colored arc but defines the placement of your button. (In your case just an arc with a larger radius.)
If you had a bezier path that was a simple cubic bezier curve, you could use the Bezier formula to calculate the x & y value of your point that way. If you were using a bezier path that consisted of multiple segments then you would need a way to get a point along an arbitrary CGPath. I don't know how to do this specifically, but it should be possible, since the system can animate layers along an arbitrary CGPath using keyframe animation.
The next option would be to use keyframe animation and animate your button along a GCPath. You should be able to animate it to an arbitrary point along it's path by manipulating the time settings of the animation. This is a little tricky and not very well documented.

Adding a CALayer to MKOverlayRenderer?

In iOS 7, MKOverlayView was replaced by MKOverlayRenderer. Before, I was able to add a UIImageView as a subview to MKOverlayView, and access the CALayer of the MKOverlayView. Now, without UIView gone in MKOverlayRenderer, I'm not sure on how to add a custom CALayer (I have a CAKeyFrameAnimation that goes through a series of pictures rapidly). How would I add a UIImageView into MKOverlayRenderer? How would I add a CALayer in a CGContextRef (used by MKOverlayRenderer)?
I tried doing it on my own, but it doesn't go through the images at all.
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context {
CGContextSaveGState(context);
CGRect test = [self rectForMapRect:self.overlay.boundingMapRect];
// Clip the drawing space to the map rect
CGRect clipRect = [self rectForMapRect:mapRect];
CGContextClipToRect(context, clipRect);
// CoreGraphics' coordinate system is flipped, so we need to transform it first
CGContextScaleCTM(context, 1.0, -1.0);
CGContextTranslateCTM(context, 0, -test.size.height);
// Draw the portion of the image in the map rect
// CGContextDrawImage(context, test, [image CGImage]);
CGContextRestoreGState(context);
CALayer *sublayer = [CALayer layer];
sublayer.frame = test;
NSArray *radarImages = [NSArray arrayWithObjects:(id)image.CGImage, image1.CGImage, image2.CGImage, image3.CGImage, image4.CGImage, image5.CGImage, image6.CGImage, image7.CGImage, image8.CGImage, image9.CGImage, image10.CGImage, image11.CGImage, image12.CGImage, image13.CGImage, image14.CGImage, image15.CGImage, image16.CGImage, image17.CGImage, image18.CGImage,image19.CGImage,image20.CGImage,image21.CGImage, image22.CGImage, image23.CGImage, image24.CGImage, nil];
anim = [CAKeyframeAnimation animation];
[anim setKeyPath:#"contents"];
[anim setCalculationMode:kCAAnimationDiscrete];
[anim setValues:radarImages];
[anim setRepeatCount:INFINITY];
[anim setDuration:2.6];
[self drawLayer:sublayer inContext:context];
[sublayer addAnimation:anim forKey:nil];
[sublayer renderInContext:context];
[sublayer setNeedsDisplay];
}
Any help is appreciated, thanks!
Try adding the following code
[[[self view] layer] addSublayer: sublayer];
and use NSMutableArray instead of NSArray.

CALayer's image not showing up

I am trying to use CALayer to display an image, but the image is not showing up. I've verified that 'image' is not nil, image.size is correct, and layer.contents is a valid CABackingStore.
- (void)viewDidLoad
{
[super viewDidLoad];
layer = [[CALayer alloc] init];
[self.view.layer addSublayer:layer];
UIImage *image = [UIImage imageNamed:#"download.jpeg"];
layer.bounds = CGRectMake(0, 0, image.size.width, image.size.height);
layer.position = self.view.center;
layer.contents = (id)image.CGImage;
[layer setNeedsDisplay];
}
What could be the problem here?
Removing [layer setNeedsDisplay]; should correct the behavior.
From the docs (emphasis added):
Calling this method causes the layer to recache its content. This results in the layer potentially calling either the displayLayer: or drawLayer:inContext: method of its delegate. The existing content in the layer’s contents property is removed to make way for the new content.
either use this
layer = [[CALayer alloc] init];
[self.view.layer addSublayer:layer];
//UIImage *image = [UIImage imageNamed:#"download.jpeg"];
layer.bounds = CGRectMake(0, 0, 300,300);
layer.position = self.view.center;
//layer.contents = (id)image.CGImage;
layer.contents = (id)[UIImage imageNamed:#"download.jpeg"].CGImage;
[layer setNeedsDisplay];
or
remove [layer setNeedsDisplay];
I guess the setNeedsDisplay made the layer to redraw the content and so deleting the image.
--

Resources