Very custom UIButton animation in iOS - ios

I have a custom button which is my own subclass of UIButton. It looks like a circled arrow, starting at some angle startAngle end finishing at some endAngle=startAngle+1.5*M_PI.
startAngle is a button's property which is then used in its drawRect: method.
I want to make this arrow to continuously rotate by 2Pi around its center when this button is pressed. So I thought that I can easily use [UIView beginAnimations: context:] but apparently it can't be used as it doesn't allow to animate custom properties. CoreAnimation also doesn't suite as it only animates the CALayer properties.
So what is the easiest way to implement an animation of a custom property of UIView subclass in iOS?
Or maybe I missed something and it is possible with already mentioned techniques?
Thank you.

Thanks to Jenox I have updated animation code using CADisplayLink which seems to be really more correct solution than NSTimer. So I show the correct implementation with CADisplayLink now. It is very close to the previous one, but even a bit simpler.
We add the QuartzCore framework to our project.
Then we put the following lines in the header file of our class:
CADisplayLink* timer;
Float32 animationDuration; // Duration of one animation loop
Float32 initAngle; // Represents the initial angle
Float32 angle; // Angle used for drawing
CFTimeInterval startFrame; // Timestamp of the animation start
-(void)startAnimation; // Method to start the animation
-(void)animate; // Method for updating the property or stopping the animation
Now in implementation file we set the values for duration of the animation and the other initial values:
initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f; // Arrow makes full 360° in 1.5 seconds
startFrame=0; // The animation hasn't been played yet
To start the animation we need to create the CADisplayLink instance which will call method animate and add it to main RunLoop of our application:
-(void)startAnimation
{
timer = [CADisplayLink displayLinkWithTarget:self selector:#selector(animate)];
[timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
This timer will call animate method every runLoop of the application.
So now comes the implementation of our method for updating the property after each loop:
-(void)animate
{
if(startFrame==0) {
startFrame=timer.timestamp; // Setting timestamp of start of animation to current moment
return; // Exiting till the next run loop
}
CFTimeInterval elapsedTime = timer.timestamp-startFrame; // Time that has elapsed from start of animation
Float32 timeProgress = elapsedTime/animationDuration; // Determine the fraction of full animation which should be shown
Float32 animProgress = timingFunction(timeProgress); // The current progress of animation
angle=initAngle+animProgress*2.f*M_PI; // Setting angle to new value with added rotation corresponding to current animation progress
if (timeProgress>=1.f)
{ // Stopping animation
angle=initAngle; // Setting angle to initial value to exclude rounding effects
[timer invalidate]; // Stopping the timer
startFrame=0; // Resetting time of start of animation
}
[self setNeedsDisplay]; // Redrawing with updated angle value
}
So unlike case with NSTimer we now don't need to calculate the time interval at which to update the angle property and redraw the button. We now only need to count how much time has passed from the start of animation and set the property to value which corresponds to this progress.
And I must admit that animation works a bit more smoothly than in case of NSTimer.
By default, CADisplayLink calls the animate method each run loop. When I calculated the frame rate, it was 120 fps. I think that it is not very efficient so I have decreased the frame rate to just 22 fps by changing the frameInterval property of CADisplayLink before adding it to mainRunLoop:
timer.frameInterval=3;
It means that it will call the animate method at first run loop, then do nothing next 3 loops, and call on the 4-th, and so on. That's why frameInterval can be only integer.

Thanks again to k06a for suggestion to use timer. I've made some study about working with NSTimer and now I want to show my implementation, since I think, it can be useful for others.
So in my case I had a UIButton subclass which was drawing a curved arrow which started from some angle Float32 angle; which is the main property from which the drawing of whole arrow starts. That means that just changing the value of angle will rotate whole arrow. So to make animation of this rotation I put the following lines in the header file of my class:
NSTimer* timer;
Float32 animationDuration; // Duration of one animation loop
Float32 animationFrameRate; // Frames per second
Float32 initAngle; // Represents the initial angle
Float32 angle; // Angle used for drawing
UInt8 nFrames; // Number of played frames
-(void)startAnimation; // Method to start the animation
-(void)animate:(NSTimer*) timer; // Method for drawing one animation step and stopping the animation
Now in implementation file I set the values for duration and frame rate of my animation and the initial angles for drawing:
initAngle=0.75*M_PI;
angle=initAngle;
animationDuration=1.5f; // Arrow makes full 360° in 1.5 seconds
animationFrameRate=15.f; // Frame rate will be 15 frames per second
nFrames=0; // The animation hasn't been played yet
To start the animation we need to create the NSTimer instance which will call method animate:(NSTimer*) timer every 1/15 seconds:
-(void)startAnimation
{
timer = [NSTimer scheduledTimerWithTimeInterval:1.f/animationFrameRate target:self selector:#selector(animate:) userInfo:nil repeats:YES];
}
This timer will call animate: method immediately and then repeat it every 1/15 second until it will be manually stopped.
So now comes the implementation of our method for animating a single step:
-(void)animate:(NSTimer *)timer
{
nFrames++; // Incrementing number of played frames
Float32 animProgress = nFrames/(animationDuration*animationFrameRate); // The current progress of animation
angle=initAngle+animProgress*2.f*M_PI; // Setting angle to new value with added rotation corresponding to current animation progress
if (animProgress>=1.f)
{ // Stopping animation when progress is >= 1
angle=initAngle; // Setting angle to initial value to exclude rounding effects
[timer invalidate]; // Stopping the timer
nFrames=0; // Resetting counter of played frames for being able to play animation again
}
[self setNeedsDisplay]; // Redrawing with updated angle value
}
The first thing I want to mention is that for me just comparison line angle==initAngle didn't work due to rounding effects. They don't are exactly the same after full rotation. That's why I check if they are just close enough and then set angle value to initial value to block small drift of angle value after many repeated animation loops.
And to be totally correct, this code must also manage conversion of angles to always be between 0 and 2*M_PI with something like this:
angle=normalizedAngle(initAngle+animProgress*2.f*M_PI);
where
Float32 normalizedAngle(Float32 angle)
{
while(angle>2.f*M_PI) angle-=2.f*M_PI;
while(angle<0.f) angle+=2.f*M_PI;
return angle
}
And another important thing is that, unfortunately, I don't know any easy way to apply easeIn, easeOut or other default animationCurves to this kind of manual animation. I think it doesn't exist. But it is, of course, possible to do it by hands. The line standing for that timing function is
Float32 animProgress = nFrames/(animationDuration*animationFrameRate);
It can be treated as Float32 y = x;, that means linear behavior, a constant speed, which is the same as speed of time. But you can modify it to be like y = cos(x) or y = sqrt(x) or y = pow(x,3.f) which will give some nonlinear behavior. You can think it yourself taking into account that x will go from 0 (start of animation) to 1 (end of animation).
For better looking code it is better to make some independent timing function:
Float32 animationCurve(Float32 x)
{
return sin(x*0.5*M_PI);
}
But now, since the dependence between animation progress and time is not linear, it's safer to use the time as indicator for stopping the animation. (You might want for example to make your arrow to make 1.5 full turns and than rotate back to the initial angle, that means your animProgress will go from 0 to 1.5 and than back to 1 while timeProgress will go from 0 to 1.)
So to be safe we separate time progress and animation progress now:
Float32 timeProgress = nFrames/(animationDuration*animationFrameRate);
Float32 animProgress = animationCurve(timeProgress);
and then check time progress to decide if should the animation stop:
if(timeProgress>=1.f)
{
// Stop the animation
}
By the way, if somebody knows some sources with list of useful timing functions for animation, I would appreciate if you share them.
Built in MacOS X utility Grapher helps a lot in visualizing the functions, so that you can see how your animation progress will depend on time progress.
Hope it helps somebody...

- (void)onImageAction:(id)sender
{
UIButton *iconButton = (UIButton *)sender;
if(isExpand)
{
isExpand = FALSE;
// With Concurrent Block Programming:
[UIView animateWithDuration:0.4 animations:^{
[iconButton setFrame:[[btnFrameList objectAtIndex:iconButton.tag] CGRectValue]];
} completion: ^(BOOL finished) {
[self animationDidStop:#"Expand" finished:YES context:nil];
}];
}
else
{
isExpand = TRUE;
[UIView animateWithDuration:0.4 animations:^{
[iconButton setFrame:CGRectMake(30,02, 225, 205)];
}];
for(UIButton *button in [viewThumb subviews])
{
[button setUserInteractionEnabled:FALSE];
//[button setHidden:TRUE];
}
[viewThumb bringSubviewToFront:iconButton];
[iconButton setUserInteractionEnabled:TRUE];
// [iconButton setHidden:FALSE];
}
}

Related

Animating Pie Progress with UIBezierPath

Problem:
If you take a look at my current code, you'll see that it works fine if targetSeconds is higher than ~2-3 seconds.
However, it will not work if targetSeconds is 0.005 seconds because there's no way it can finish 100 method calls in 0.005 seconds. Therefore, does anyone have any suggestions to what I can do to improve it? I'd rather not include third party GitHub repositories.
Current code:
// Target seconds means the seconds that it'll take to become 100.0f.
- (void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds{
// Try to set timeInterval to 0.005f and you'll see that it won't finish in 0.005f
[NSTimer scheduledTimerWithTimeInterval:targetSeconds / 100.0f target:self selector:#selector(animateWithTimer:) userInfo:nil repeats:YES];
}
- (void)animateWithTimer:(NSTimer *)timer {
BOOL isFinished = self.currentProgress >= 100;
if (isFinished) {
// Invalidate timer
if (timer.isValid) {
[timer invalidate];
}
// Reset currentProgress
self.currentProgress = 0.0f;
}else{
if (timer.isValid) {
self.currentProgress += 1;
}
}
}
// Overriden setter
- (void)setCurrentProgress:(CGFloat)currentProgress {
if (_currentProgress == currentProgress) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
_currentProgress = currentProgress;
[self setNeedsDisplay];
});
}
And then in drawRect, I have an UIBezierPath that basically draws the circle depending on self.currentProgress.
Something like this: CGFloat endAngle = (self.currentProgress / 100.0f) * 2 * M_PI + startAngle;
Question:
Is there any formula or anything that'll help me in my case? Because if I were to set self.currentProgress to self.currentProgress += 5; instead of 1, it'll animate a lot faster, which is precisely what I'm looking for.
First of all, when would you want to redraw every 0.005 seconds? That's 200 FPS, way more than you need.
Don't reinvent the wheel – leverage Core Animation! Core Animation already knows how to call your state change function at the proper rate, and how to redraw views as necessary, assuming you tell it what to do. The gist of this strategy is as follows:
Add a dynamic property to your layer that represents the completeness of your pie slice.
Tell Core Animation that this property can be animated by either overriding actionForKey:, or setting the animation into the actions dictionary (or even more options, detailed here).
Tell Core Animation that changes to this property require redraws of the layer using needsDisplayForKey:.
Implement the display method to redraw the pie based on the presentation layer's value of your dynamic property.
Done! Now you can animate the dynamic property from any value to any other, just as you would opacity, position, etc. Core Animation takes care of the timing and the callbacks, and you get a buttery smooth 60 FPS.
For some examples, see the following resources, listed in order of decreasing usefulness (in my opinion):
Animating Pie Slices using a custom CALayer – this is basically what you want to do
Animating Custom Layer Properties – better written but a bit less applicable
Apple's Core Animation Guide – esoteric, but worth a read if you want to master the strange beast that is Core Animation
Good luck!
I prefer use something like this, because timer (and usleep) on small intervals works very inaccurately:
-(void)startAnimatingWithTargetSeconds:(NSTimeInterval)targetSeconds
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
float fps = 60;
float currentState = 0;
float frameStateChange = fps/targetSeconds;
NSDate *nextFrameDate = [NSDate date];
while (currentState < 1) {
currentState += frameStateChange;
self.currentProgress = roundf(currentState * 100.);
nextFrameDate = [nextFrameDate dateByAddingTimeInterval:1./fps];
while ([nextFrameDate timeIntervalSinceNow] > 0) {
usleep((useconds_t)(100000/fps));
}
}
self.currentProgress = 0;
});
}

How to both scroll and stop UIScrollView programmatically?

There are a lot of similar questions but they all differ from this one.
I have UIScrollView which I could both scroll and stop programmatically.
I scroll via the following code:
[UIView animateWithDuration:3
delay:0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{ [self.scrollView scrollRectToVisible:newPageRect animated:NO]; }];
And I don't know how to stop it at all. In all the cases it won't stop or will stop but it also jumps to newPageRect (for example in the case of removeAllAnimations).
Could you suggest how to stop it correctly? Should I possibly change my code for scrolling to another one?
I think this is something you best do yourself. It may take you a few hours to create a proper library to animate data but in the end it can be very rewarding.
A few components are needed:
A time bound animation should include either a CADispalyLink or a NSTimer. Create a public method such as animateWithDuration: which will start the timer, record a current date and set the target date. Insert a floating value as a property which should then be interpolated from 0 to 1 through date. Will most likely look something like that:
- (void)onTimer {
NSDate *currentTime = [NSDate date];
CGFloat interpolation = [currentTime timeIntervalSinceDate:self.startTime]/[self.targetTime timeIntervalSinceDate:self.startTime];
if(interpolation < .0f) { // this could happen if delay is implemented and start time may actually be larger then current
self.currentValue = .0f;
}
else if(interpolation > 1.0f) { // The animation has ended
self.currentValue = 1.0f;
[self.displayLink invalidate]; // stop the animation
// TODO: notify owner that the animation has ended
}
else {
self.currentValue = interpolation;
// TODO: notify owner of change made
}
}
As you can see from the comments you should have 2 more calls in this method which will notify the owner/listener to the changes of the animation. This may be achieved via delegates, blocks, invocations, target-selector pairs...
So at this point you have a floating value interpolating between 0 and 1 which can now be used to interpolate the rect you want to be visible. This is quite an easy method:
- (CGRect)interpolateRect:(CGRect)source to:(CGRect)target withScale:(CGFloat)scale
{
return CGRectMake(source.origin.x + (target.origin.x-source.origin.x)*scale,
source.origin.y + (target.origin.y-source.origin.y)*scale,
source.size.width + (target.size.width-source.size.width)*scale,
source.size.height + (target.size.height-source.size.height)*scale);
}
So now to put it all together it would look something like so:
- (void)animateVisibleRectTo:(CGRect)frame {
CGRect source = self.scrollView.visibleRect;
CGRect target = frame;
[self.animator animateWithDuration:.5 block:^(CGFloat scale, BOOL didFinish) {
CGRect interpolatedFrame = [Interpolator interpolateRect:source to:target withScale:scale];
[self.scrollView scrollRectToVisible:interpolatedFrame animated:NO];
}];
}
This can be a great system that can be used in very many systems when you want to animate something not animatable or simply have a better control over the animation. You may add the stop method which needs to invalidate the timer or display link and notify the owner.
What you need to look out for is not to create a retain cycle. If a class retains the animator object and the animator object retains the listener (the class) you will create a retain cycle.
Also just as a bonus you may very easily implement other properties of the animation such as delay by computing a larger start time. You can create any type of curve such as ease-in, ease-out by using an appropriate function for computing the currentValue for instance self.currentValue = pow(interpolation, 1.4) will be much like ease-in. A power of 1.0/1.4 would be a same version of ease-out.

How can my iOS app animate a sequence of colors?

I have a pre-defined sequence of colors and their durations (ms) that I would like to display in order (animate?) in a subview. In Android, I can use addFrame() with an AnimationDrawable to create an animation object. Is there something similar that will do this for me in iOS? (The total duration must be the sum of the individual color durations.) Thanks!
There are multiple ways you could do this. One example:
Loop over your colours and durations, say they are in an array of dictionaries (not required)
Keep a count of the duration so far
On each iteration, create an animation using + UIView animateWithDuration:delay:options:animations:completion: where the delay is the accumulated duration, the duration is the duration for this iteration and in the animation block:
The animation block sets the backgroundColor of the view
This approach has limitations as you can't stop or pause it.
Alternatively, use the completion block to trigger the next step of the animation. Now you can check if you should be stopping, and you don't need to accumulate the duration.
Or, use a CAPropertyAnimation key path animation. Now you can cancel at any point in the animation and you have full control over the speed and style of transition between colours.
The simplest way would be using a UIView object, and setting the background color for a certain duration. If the duration is the same you can use an NSTimer to fire repeatedly, setting the view to a new color each time.
Here is the simple example of animation using completion handler. _durations is an array of NSNumber objects representing color durations. _colors - array of UIColor instances.
- (void)animate
{
[UIView
animateWithDuration:[_durations[_currentColor] doubleValue]
animations:^{
self.view.backgroundColor = _colors[_currentColor++];
}
completion:^(BOOL finished) {
if (_currentColor < _colors.count) {
[self animate];
}
else {
_currentColor = 0;
}
}];
}

NStimer and setNeedsDisplay

i have some problem with nstimer and ui update and drawrect,
I have a viewcontroller and put a timer inside, it runs every 0.02 secs,
in this timer tick function, I make an imageview move from top to bottom( change centre of view),
then add another view on, and when touch begin touchmove and touchend on this view,
draw a line and call setneedsdisplay, when my fingure moves on the view, the imageview i mentioned before, moves slower,
by checking the time tick, I found that, without finger on, it ticks 0.02 secs, but when
move on, it slows to abt 0.1 sec, which make the imageview move slower,
any other way to optimise it,
I think the setneedsdisplay did the thick,
ofcourse, I try to change the runloop mode with
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];
not help.
pls help, and another question is will another thread help this?
I tried nsthread, seems not help.... lol
For this purpose a much better solution is a CADisplayLink, that calls a method each time a frame is going to be refreshed. That way you'll avoid desynchronization between the timer and the actual framerate.
You set up a display link e.g. like this:
_dl = [CADisplayLink displayLinkWithTarget: self selector:#selector(refreshFrame)];
[_dl addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_dl setPaused:NO];
and in the refreshFrame callback you can update the screen. You can make use of the duration and frameInterval to calculate the time that passed from the last refresh and adjust the movement speed accordingly:
-(void) refreshFrame
{
CGFloat speed = 10.0; //points per second
CGFloat timePassed = (_dl.duration * _dl.frameInterval)];
//refresh the view (example)
CGPoint imageCenter = self.imageView.center;
imageCenter y += speed * timePassed;
self.imageView.center = imageCenter;
}
This way the speed of the view stays constant even as the framerate changes in time.
It is wrong to assume the timer event is going to be called with exact intervals. Calculate the new parameters based on the speed and velocity vectors and time from the last update.

Updating running animations smoothly

I have a number of images I run continuous animations on. How does one update these running animations smoothly so that there is no visible transition between them, unless part of the animation.
e.g. rotating and scaling an image, and then updating the animation to rotate as it was, but scale up slightly. I currently get a visible change.
I can imagine that that scaling should be done within an animation block and them just run the rotation animation as before, the issue would be then that the rotation would stop while it scales up.
I need it to be seamless. e.g. this code block does not cause a smooth scaling, even though it uses UIViewAnimationOptionBeginFromCurrentState
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut //| UIViewAnimationOptionRepeat
animations:(void (^)(void)) ^{
imageViewToScale.transform = CGAffineTransformMakeScale(1.2, 1.2);
}
completion:^(BOOL finished){
//imageViewToScale.transform=CGAffineTransformIdentity;
}];
To be honest if your going to have continuous animations on your views that will alter there states at run time UIView animations isn't he best approach and might cause jitter whilst the completion handler is called over and over e.t.c.
However is you want to do 2 animations in the block you can either set the scale and the rotation in the same block and they will happend simultaneously. Or you could call a function that starts the block and on completion calls it again to see if there are any new animations for that view. When there are none, the completion block just stops, sort of using them recursively, though this isn't the approach i wouldn't recommend if you are doing a lot of animations on a lot of views continuouly.
In OpenGLES you use NSTimer to run continuous animation that would handle the updating of all animations in your app. If you go this route its a lot more hard work and you need to implement the easing/quadratic curve functions for smooth animation yourself. However if you extend the clases you could set states for each image and then when the timer fires you could update its transformation based one the states you have given it. So when it has rotated a certain amount of degrees/radians then you could set it to scale. Now to keep animation smoothly you will need to use NSTimerIntervals to make sure you multiply the distance moved by time elapsed in order to get smooth animation. Personally this is the route i use for things that might be moving on screen constantly but might be over kill if you need to only move things twice and then be done.
EDIT: The code for doing the second step as you asked!
So you need to declare a NSTimer that wil poll your animation steps and an NSTimeInterval so that you update your animation each step only by the amount of time that has passed.
NSTimer *animationTimer;
NSTimeInterval animationInterval;
NSTimeInterval lastUpdateTime;
float currentRotation;
float current scale;
Thirdly you need to set up an NSTimer to fire off updating of your views:
- (void)startAnimation
{
animationTimer = [NSTimer scheduledTimerWithTimeInterval:animationInterval target:self selector:#selector(drawView:) userInfo:nil repeats:YES];
}
- (void)stopAnimation
{
[animationTimer invalidate];
animationTimer = nil;
}
Then you need to kick the thing off when your view starts or when you want to start anmation something like this works
- (void)setAnimationInterval:(NSTimeInterval)interval
{
animationInterval = interval;
if(animationTimer)
{
[self stopAnimation];
[self startAnimation];
}
}
Then In your drawView: Method you need to update your transforms based on time elapsed between each fire of the timer so that the animation is smooth and constant over time. Essentially a linear transform at this point.
- (void)drawView:(id)sender
{
if(lastUpdateTime == -1)
{
lastUpdateTime = [NSDate timeIntervalSinceReferenceDate];
}
NSTimeInterval timeSinceLastUpdate = [NSDate timeIntervalSinceReferenceDate] - lastUpdateTime;
currentRotation = someArbitrarySCALEValue * timeSinceLastUpdate;
currentScale = someArbitraryROTATIONValue * timeSinceLastUpdate;
CGAffineTransform scaleTrans = CGAffineTransformMakeScale(currentScale,currentScale);
CGAffineTransform rotateTrans = CGAffineTransformMakeRotation(currentRotation * M_PI / 180);
imageViewToScale.transform = CGAffineTransformConcat(scaleTrans, rotateTrans);
lastUpdateTime = [NSDate timeIntervalSinceReferenceDate];
}
Note this will not add easing to your animations nor will it at physics to the stopping you will need to play around with that. Also you may need to make sure that when the rotation goes over 360 you reset it to 0 and that you convert between degrees and radians respectively.
Look into using physics for things like bounces, and friction to make things slow nicely.
Look into quadratic graphs for easing to make things move smoothly over time, quadratic interpolation essentially.

Resources