How to customize the shake animation in IOS's UIImageView - ios

I'm coding a shake image function, and the code is below:
CABasicAnimation* shake = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
shake.fromValue = [NSNumber numberWithFloat:-0.2];
shake.toValue = [NSNumber numberWithFloat:+0.2];
shake.duration = 0.1;
shake.autoreverses = YES;
shake.repeatCount = 4;
[self.imageView.layer addAnimation:shake forKey:#"imageView"];
self.imageView.alpha = 1.0;
[UIView animateWithDuration:2.0 delay:2.0 options:UIViewAnimationOptionCurveEaseIn animations:nil completion:nil];
I have two questions:
1.the code above will shake the image around its center(transform.rotation.z).But how to make the image shake around the bottom center?
2.How can I control the start and stop of this shaking animation? For example, make the image shaking until get data from server finished then stop the animation.

you could do this by modifying the image. Make it twice as tall but have the bottom just be transparent.
A similar solution that only uses code:
Put the ImageView into another view...call it imageHolderView where imageHolderView is twice as tall as the image, but it otherwise just transparent, and then shake that instead of just your image

Related

Create smooth animation with UIImageView animation

I'm currently animating between 4 images like this:
UIImageView *tom3BeforeImage;
tom3Images = [NSArray arrayWithObjects: [UIImage imageNamed:#"floortom_before1.png"],[UIImage imageNamed:#"floortom_before2.png"],[UIImage imageNamed:#"floortom_before3.png"],[UIImage imageNamed:#"floortom_before4.png"], nil ];
tom3BeforeImage.animationImages = tom3Images;
tom3BeforeImage.animationDuration = 0.75;
[tom3BeforeImage startAnimating];
It works fine, except that the animation is choppy between the images. I need the duration to be exactly .75 seconds, so speeding it up is not an option.
What's the best way to have the animation be smoother between the images, kind of like blending between each image change?
Thanks!
If you're using frame based UIImageView animation, and the animation must be .75 seconds, then the only way I know of to make it smoother is to create more frames. Try 30 frames/second, or about 22 frames. That should give very smooth motion.
If you want some sort of cross-dissolve between frames then you won't be able to use UIView frame animation. you'll have to use UIView block animation (using animateWithDuration:animations: or its cousins.)
You could create a sequence of cross-dissolves between your frames where the total duration of the sequence is .75 seconds. Have each transition trigger the next transition in it's completion block.
Something like this:
You'll need 2 image views, stacked on top of each other. You'll fade one out and the other in at the same time. You'll need to set the opaque flag to NO on both.
Lets call them tom3BeforeImage1 and tom3BeforeImage2
Add an int instance variable imageCount and make your array of images, tom3Images, and instance variable as well:
- (void) animateImages;
{
CGFloat duration = .75 / ([tom3Images count] -1);
//Start with the current image fully visible in tom3BeforeImage1
tom3BeforeImage1.image = tom3Images[imageCount];
tom3BeforeImage1.alpha = 1.0;
//Get the next image ready, at alpha 0, in tom3BeforeImage2
tom3BeforeImage2.image = tom3Images[imageCount+1];
tom3BeforeImage2.alpha = 0;
imageCount++
[UIView animateWithDuration: duration
delay: 0
options: UIViewAnimationOptionCurveLinear
animations:
^{
//Fade out the current image
tom3BeforeImage1.alpha = 0.0;
//Fade in the new image
tom3BeforeImage2.alpha = 1.0;
}
completion:
^{
//When the current animation step completes, trigger the method again.
if (imageCount < [tom3Images count] -1)
[self animateImages];
}
];
}
Note that I banged out the code above in the forum editor without having had enough coffee. It likely contains syntax errors, and may even have logic problems. This is just to get you thinking about how to do it.
Edit #2:
I'm not sure why, but I decided to flesh this out into a full-blown example project. The code above works passably well after debugging, but since it's fading one image out at the same time it's fading another one in, the background behind both image views shows through.
I reworked it to have logic that only fades the top image in and out. It puts the first frame in the top image view and the second frame in the bottom image view, then fades out the top image view.
The project is up on github, called Animate-Img. (link)
Then it installs the third frame in the top image view and fades it IN,
Then it installs the 4th fame in the bottom image view and fades out the top to expose the bottom, etc, etc.
I ended up creating a generalized method
- (void) animateImagesWithDuration: (CGFloat) totalDuration
reverse: (BOOL) reverse
crossfade: (BOOL) doCrossfade
withCompletionBlock: (void (^)(void)) completionBlock;
It will animate a set of images, into a pair of image views, optionally reversing the animation once it's done. It takes a completion block that gets called once the animation is finished.
The animate button actually calls a method that repeats the whole animation sequence. It's currently set to only run it once, but changing a constant will make the program repeat the whole sequence, if desired.
I do had the requirement to have animation with array of images. Initially when i used animationImages property of imageview, I got the desired animation but the transition between the images were not smooth, I then used CAKeyframeAnimation to achieve the smooth transition, the catch is to use timingFunction along with correct calculationMode. I am not sure this is the exact the answer for the question but this is one way to make the animation smoother,
below is the code for that
For more info on calculation mode please see Apple Documentation
- (void) animate
{
NSMutableArray * imageArray = [[NSMutableArray alloc] init];
int imageCount = 8;
for (int i=0; i<=imageCount; i++) {
[imageArray addObject:(id)[UIImage imageNamed:[NSString stringWithFormat:#“image%d”,i]].CGImage];
}
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
animation.calculationMode = kCAAnimationLinear;// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = 8.0;
animation.values = imageArray;
animation.repeatCount = 1; // Change it for repetition
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards; // To keep the last frame when animation ends
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[imageView.layer addAnimation:animation forKey:#"animation"];
}
UPDATE- Swift 3
func animate() {
var imageArray = [CGImage]()
let imageCount: Int = 3
for i in 0...imageCount {
imageArray.append((UIImage(named: String(format:"image\(i)"))?.cgImage!)!)
}
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = kCAAnimationLinear
// Make sure this is kCAAnimationLinear or kCAAnimationCubic other mode doesnt consider the timing function
animation.duration = CFTimeInterval(floatLiteral: 8.0)
animation.values = imageArray
animation.repeatCount = 1
// Change it for repetition
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
// To keep the last frame when animation ends
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animImageView.layer.add(animation, forKey: "animation")
}

UIView rotation and Subview (Button) touch

I have a UIView and on this UIView I have 8 buttons. I need to rotate this UIVIew every time So I am using the code below:
CABasicAnimation *fullRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
fullRotation.fromValue = [NSNumber numberWithFloat:0];
fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
fullRotation.duration = 30;
fullRotation.repeatCount = MAXFLOAT;
[self.menuView.layer addAnimation:fullRotation forKey:#"360"];
It rotates my view properly, But I am unable to get touch on rotating buttons. Since the Parent view is rotating and the Buttons will also rotate, Touch isn't working. Thanks
Because you rotate layer. I think you can't do what you need, but try to use CGAffineTransform
CGAffineTransform transform //desired transform
[UIView animateWithDuration:time
animations:^{menuView.transform = transform;}];
and call it by timer.
Normal view touches do not work correctly during an animation. Animation only makes the views look like they are moving. Their actual coordinates don't move.
If you want to support tapping on objects you will need to roll your own tap handling code at the superview level using the presentation layer's hitTest method.
I have a sample project on github that shows how to do that:
iOS Core Animation demo

Rotating UIButton

I've been trying to rotate a button using the following method:
-(IBAction)rotate:(id)sender{
CGPoint pencilCenter = pencil.center;
[pencil setCenter:pencilCenter];
CGFloat floater = 1.0;
[UIView animateWithDuration:0.7 animations:^(void){
[pencil setTransform:CGAffineTransformMakeRotation(floater)];
}];
[UIView animateWithDuration:0.7 animations:^(void){
[pencil setTransform:CGAffineTransformMakeRotation(floater)];
}];
}
This is supposed to make the button do some kind of "shake", then it's supposed to be back in its original position- yet all it does is changing the button's location, moving it to only one side and on another run of the method the button doesn't react at all.
What's the problem with my code?
Thanks!
EDIT 2:
My que is- how do I make a button to do a little shake/wiggle ,e.g. the wiggle app mode when editing sptingboard.
Using this code is giving me rotation to left, smooth animates from left to right then right to left, then rotates to original position. Now, I want this not to just rotate, but do this with an animation, like a wiggle.
[UIView animateWithDuration:0.25
delay:0.0
options:(UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveLinear | UIViewAnimationOptionAutoreverse)
animations:^ {
pencil.transform = CGAffineTransformMakeRotation(DEGREES_TO_RADIANS(30));
pencil.transform = CGAffineTransformIdentity;
}
completion:^(BOOL finished){
}
];
Thanks!
Import "QuartzCore/QuartzCore.h" and try this,
CABasicAnimation *fullRotation;
fullRotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
fullRotation.delegate = self;
fullRotation.fromValue = [NSNumber numberWithFloat:0];
fullRotation.toValue = [NSNumber numberWithFloat:((360*M_PI)/180)];
fullRotation.duration = 1.7;
fullRotation.repeatCount = 2;
[btnTemp.layer addAnimation:fullRotation forKey:#"360"];
Try using CGAffineTransformRotate instead of CGAffineTransformMakeRotation. You can use `CGAffineTransformIdentity1 as the first argument in all calls, so the final transform according to second argument will be applied on the original shape(?) of the frame.
While it didn't perfectly answer the question, Shardul's answer is great to rotate the button (2 full rotations). So here it is, converted to Swift 3:
let fullRotation = CABasicAnimation(keyPath: "transform.rotation")
fullRotation.delegate = self
fullRotation.fromValue = NSNumber(floatLiteral: 0)
fullRotation.toValue = NSNumber(floatLiteral: Double(CGFloat.pi * 2))
fullRotation.duration = 0.5
fullRotation.repeatCount = 2
button.layer.add(fullRotation, forKey: "360")
You will need to import QuartzCore:
import QuartzCore
And your ViewController needs to conform to CAAnimationDelegate:
class ViewController: UIViewController, CAAnimationDelegate {
}
Well, the docs say: "...the specified animations are started immediately on another thread ...". Now, all UIKit code is NOT thread safe. So, maybe it helps if instead calling your UI setters (setTransform, setCenter) from the completion block (which runs in a separate thread) to call them using performSelectorOnMainThread:withObject:.

How to stop and reverse a UIView animation?

I have animated a UIView so that it shrinks when the user touches a toggle button and it expands back to its original size when the user touches the button again. So far everything works just fine. The problem is that the animation takes some time - e.g. 3 seconds. During that time I still want the user to be able to interact with the interface. So when the user touches the button again while the animation is still in progress the animation is supposed to stop right where it is and reverse.
In the Apple Q&As I have found a way to pause all animations immediately:
https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html
But I do not see a way to reverse the animation from here (and omit the rest of the initial animation). How do I accomplish this?
- (IBAction)toggleMeter:(id)sender {
if (self.myView.hidden) {
self.myView.hidden = NO;
[UIView animateWithDuration:3 animations:^{
self.myView.transform = expandMatrix;
} completion:nil];
} else {
[UIView animateWithDuration:3 animations:^{
self.myView.transform = shrinkMatrix;
} completion:^(BOOL finished) {
self.myView.hidden = YES;
}];
}
}
In addition to the below (in which we grab the current state from the presentation layer, stop the animation, reset the current state from the saved presentation layer, and initiate the new animation), there is a much easier solution.
If doing block-based animations, if you want to stop an animation and launch a new animation in iOS versions prior to 8.0, you can simply use the UIViewAnimationOptionBeginFromCurrentState option. (Effective in iOS 8, the default behavior is to not only start from the current state, but to do so in a manner that reflects both the current location as well as the current velocity, rendering it largely unnecessary to worry about this issue at all. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)
[UIView animateWithDuration:3.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
// specify the new `frame`, `transform`, etc. here
}
completion:NULL];
You can achieve this by stopping the current animation and starting the new animation from where the current one left off. You can do this with Quartz 2D:
Add QuartzCore.framework to your project if you haven't already. (In contemporary versions of Xcode, it is often unnecessary to explicitly do this as it is automatically linked to the project.)
Import the necessary header if you haven't already (again, not needed in contemporary versions of Xcode):
#import <QuartzCore/QuartzCore.h>
Have your code stop the existing animation:
[self.subview.layer removeAllAnimations];
Get a reference to the current presentation layer (i.e. the state of the view as it is precisely at this moment):
CALayer *currentLayer = self.subview.layer.presentationLayer;
Reset the transform (or frame or whatever) according to the current value in the presentationLayer:
self.subview.layer.transform = currentLayer.transform;
Now animate from that transform (or frame or whatever) to the new value:
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
Putting that all together, here is a routine that toggles my transform scale from 2.0x to identify and back:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CALayer *currentLayer = self.subview.layer.presentationLayer;
[self.subview.layer removeAllAnimations];
self.subview.layer.transform = currentLayer.transform;
CATransform3D newTransform;
self.large = !self.large;
if (self.large)
newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
else
newTransform = CATransform3DIdentity;
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
}
Or if you wanted to toggle frame sizes from 100x100 to 200x200 and back:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CALayer *currentLayer = self.subview.layer.presentationLayer;
[self.subview.layer removeAllAnimations];
CGRect newFrame = currentLayer.frame;
self.subview.frame = currentLayer.frame;
self.large = !self.large;
if (self.large)
newFrame.size = CGSizeMake(200.0, 200.0);
else
newFrame.size = CGSizeMake(100.0, 100.0);
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.frame = newFrame;
}
completion:NULL];
}
By the way, while it generally doesn't really matter for really quick animations, for slow animations like yours, you might want to set the duration of the reversing animation to be the same as how far you've progressed in your current animation (e.g., if you're 0.5 seconds into a 3.0 second animation, when you reverse, you probably don't want to take 3.0 seconds to reverse that small portion of the animation that you have done so far, but rather just 0.5 seconds). Thus, that might look like:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CFTimeInterval duration = kAnimationDuration; // default the duration to some constant
CFTimeInterval currentMediaTime = CACurrentMediaTime(); // get the current media time
static CFTimeInterval lastAnimationStart = 0.0; // media time of last animation (zero the first time)
// if we previously animated, then calculate how far along in the previous animation we were
// and we'll use that for the duration of the reversing animation; if larger than
// kAnimationDuration that means the prior animation was done, so we'll just use
// kAnimationDuration for the length of this animation
if (lastAnimationStart)
duration = MIN(kAnimationDuration, (currentMediaTime - lastAnimationStart));
// save our media time for future reference (i.e. future invocations of this routine)
lastAnimationStart = currentMediaTime;
// if you want the animations to stay relative the same speed if reversing an ongoing
// reversal, you can backdate the lastAnimationStart to what the lastAnimationStart
// would have been if it was a full animation; if you don't do this, if you repeatedly
// reverse a reversal that is still in progress, they'll incrementally speed up.
if (duration < kAnimationDuration)
lastAnimationStart -= (kAnimationDuration - duration);
// grab the state of the layer as it is right now
CALayer *currentLayer = self.subview.layer.presentationLayer;
// cancel any animations in progress
[self.subview.layer removeAllAnimations];
// set the transform to be as it is now, possibly in the middle of an animation
self.subview.layer.transform = currentLayer.transform;
// toggle our flag as to whether we're looking at large view or not
self.large = !self.large;
// set the transform based upon the state of the `large` boolean
CATransform3D newTransform;
if (self.large)
newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
else
newTransform = CATransform3DIdentity;
// now animate to our new setting
[UIView animateWithDuration:duration
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
}
There is a common trick you can use to do this, but it is necessary to write a separate method to shrink (and another similar one to expand):
- (void) shrink {
[UIView animateWithDuration:0.3
animations:^{
self.myView.transform = shrinkALittleBitMatrix;
}
completion:^(BOOL finished){
if (continueShrinking && size>0) {
size=size-1;
[self shrink];
}
}];
}
So now, the trick is to break the 3 seconds animation of shrinking into 10 animations (or more than 10, of course) of 0.3 sec each in which you shrink 1/10th of the whole animation: shrinkALittleBitMatrix. After each animation is finished you call the same method only when the bool ivar continueShrinking is true and when the int ivar size is positive (the view in full size would be size=10 and the view with minimum size would be size=0). When you press the button you change the ivar continueShrinking to FALSE, and then call expand. This will stop the animation in less than 0.3 seconds.
Well, you have to fill the details but I hope it helps.
First: how to remove or cancel a animation with view?
[view.layer removeAllAnimations]
if the view have many animations, such as, one animation is move from top to bottom, other is move from left to right;
you can cancel or remove a special animation like this:
[view.layer removeAnimationForKey:#"someKey"];
// the key is you assign when you create a animation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"someKey"];
when you do that, animation will stop, it will invoke it's delegate:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
if flag == 1, indicate animation is completed.
if flag == 0, indicate animation is not completed, it maybe cancelled、removed.
Second: so , you can do what you want to do in this delegate method.
if you want get the view's frame when the remove code excute, you can do this:
currentFrame = view.layer.presentationlayer.frame;
Note:
when you get the current frame and remove animation , the view will also animate a period time, so currentFrame is not the last frame in the device screen.
I cann't resolve this question at now. if some day I can, I will update this question.

Return view to original position when ending UIView animation

I have a very simple UIView animation, which causes my view to "throb":
[UIView animateWithDuration:0.2 delay:0.0f options:UIViewAnimationOptionAllowUserInteraction+UIViewAnimationOptionRepeat+UIViewAnimationOptionAutoreverse animations:^{
CGAffineTransform transf = CGAffineTransformScale(self.view.transform, 1.05f, 1.05f);
[self.view setTransform:transf];
} completion:nil];
At some point the user hits a button to cancel the animation, and apply a new transform.
[self.view.layer removeAllAnimations];
[self.view setTransform:aNewTransform];
I'd like it to reset to it's original transform, but instead it's getting increased in size by 5%.
Edit: I tried adding a completion block that resets the transform to it's original position. This works, but causes the transform I run immediately after to be trampled... the completion block gets run AFTER I apply aNewTransform.
Edit 2: I found a solution, using CABasicAnimation, instead of UIView animations. I would still be interested if anybody found a solution using UIView animations... I like the block-based interface better. This also only works, because I happen to be keeping track of my scale value separate from the one applied to the view. Everything that changes the scale uses a method that also changes self.scale
My replacement animation:
CABasicAnimation *basicAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
basicAnimation.toValue = [NSNumber numberWithFloat:self.scale*1.05f];
basicAnimation.fromValue = [NSNumber numberWithFloat:self.scale];
basicAnimation.autoreverses = YES;
basicAnimation.duration = 0.2;
basicAnimation.repeatCount = HUGE_VALF;
[self.view.layer addAnimation:basicAnimation forKey:#"Throb"];
Before starting a new animation on an existing view, you can reset the view if any of these attributes were previously altered:
// first, don't forget to stop ongoing animations for the view
[theView.layer removeAllAnimations];
// if the view was hidden
theView.hidden = NO;
// if you applied a transformation e.g. translate, scale, rotate..., reset to identity
theView.transform = CGAffineTransformIdentity;
// if you changed the anchor point, this will reset it to the center of the view
theView.layer.anchorPoint = CGPointMake(0.5, 0.5);
// if you changed the alpha, this will reset it to visible
theView.alpha = 1.;
Try putting the line
[self.view setTransform:aNewTransform];
in the completion block.

Resources