How to replace one UI element by another with custom animation? - ios

I want to implement nested commentaries(like stickers) in my own document viewer.
At first, it should be UITextView, but when resignFirstResponder executes, it should become just a small button.
The main question is: how to animate this?
I've read Quartz 2d programming guide from Apple, but it didn't gave me any ideas.
I don't asking for exact or ready solution: keywords, links to articles or documentation are enough.
Thanks for future responses.

You could use this method
[UIView animateWithDuration: delay: options: animations: completion:];
So if you wanted to fade in a button and fade out the textfield it would be
//Starting properties
myButton.alpha = 0;
myTextField.alpha = 1;
//Do the animation
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationCurveEaseInOut animations:^{
myButton.alpha = 1;
myTextField.alpha = 0;
} completion:^(BOOL finished) {
if (finished) {
NSLog(#"finished animating");
}
}];
This will change the opacity of the 2 objects from 0 - 1 / 1 - 0 over 300ms
You can animate many properties this way like size, position, opacity etc.

You could do it the same way that Apple fades between two different elements in QuickLook. You can see the effect yourself in slow-motion by pressing shift+space with an item selected in Finder.
The animation basically is a cross-fade at the same time that the frame changes. You should probably render both the button and the text view into images and do the animation with two image views (that only exist during the animation) to be able to stretch the images as the aspect ratio changes when the frames change.
You will need Core Animation to render the views into images but the rest of the images are only frame and alpha/opacity animations so you should be able to do them with UIView animations (if Core Animation seems to complicated for you). Though Core Animation will give you some more fine grained control when it comes to tweaking the animation.

Related

Crossfade multiple UIViews without background peeking through alpha?

I need to crossfade from one set of views to another set of views. There is a background sometimes behind these views. If I do a naive crossfade with animateWithDuration: v1.alpha = 1.0, v2.alpha = 0.0, then during the animation you will briefly see the background, because two views with alpha 0.5 stacked on top of each other don't have a final alpha of 1.0, they have a final alpha of I believe 0.75, hence you can see the background.
There is a UIView method transitionFromView:toView:... that allows you to achieve an ideal cross fade if you specify the animation option CrossDissolve, however, this method fails if you run two of them at once, even on fully separate views, which is a requirement. That is, if I start transitionFromView from A to B, and then transitionFromView from C to D right after, the first one will crap out.
Is there another way to achieve a proper crossfade that won't break? Does CoreAnimation have any libraries for proper crossfading? Or is there some trickery I can use to achieve it with regular UIView animations?
I assume v1 is above v2 (after it in the subviews array if they have the same parent), and you want v1 to appear and v2 to disappear.
There's no need to animate the alpha v2 at all. Just animate the alpha of v1 from 0 to 1. When the animation ends, you can set v2's alpha to 0 if you want, and remove it from the view hierarchy.
[UIView animateWithDuration:0.2 animations:^{
v1.alpha = 1;
} completion:^{
v2.alpha = 0;
[v2 removeFromSuperview];
}];
This is how you cross dissolve between two views:
UIView.transition(from: fromView, to: toView, duration: 0.3, options: .transitionCrossDissolve) { (_ finished) in
// Completion block here
}

How to properly animate UIScrollView contentOffset

I have UIScrollView subclass. Its content is reusable - about 4 or 5 views are used to display hundreds of elements (while scrolling hidden objects reused and jumps to another position when its needed to see them)
What i need: ability to automatically scroll my scroll view to any position. For example my scroll view displays 4th, 5th and 6th element and when I tap some button it needs to scroll to 30th element. In other words I need standard behaviour of UIScrollView.
This works fine:
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
but I need some customisation. For example, change animation duration, add some code to perform on end of animation.
Obvious decision:
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
} completion:^(BOOL finished) {
//some code
}];
but I have some actions connected to scroll event, and so now all of them are in animation block and it causes all subview's frames to animate too (thanks to few reusable elements all of them animates not how i want)
The question is: How can I make custom animation (in fact I need custom duration, actions on end and BeginFromCurrentState option) for content offset WITHOUT animating all the code, connected to scrollViewDidScroll event?
UPD:
Thanks to Andrew's answer(first part) I solved issue with animation inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[UIView performWithoutAnimation:^{
[self refreshTiles];
}];
}
But scrollViewDidScroll must (for my purposes) executes every frame of animation like it was in case of
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
However, now it executes only once at start of animation.
How can I solve this?
Did you try the same approach, but with disabled animation in scrollViewDidScroll ?
On iOS 7, you could try wrapping your code in scrollViewDidScroll in
[UIView performWithoutAnimation:^{
//Your code here
}];
on previous iOS versions, you could try:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Your code here
[CATransaction commit];
Update:
Unfortunately that's where you hit the tough part of the whole thing. setContentOffset: calls the delegate just once, it's equivalent to setContentOffset:animated:NO, which again calls it just once.
setContentOffset:animated:YES calls the delegate as the animation changes the bounds of the scrollview and you want that, but you don't want the provided animation, so the only way around this that I can come up with is to gradually change the contentOffset of the scrollview, so that the animation system doesn't just jump to the final value, as is the case at the moment.
To do that you can look at keyframe animations, like so for iOS 7:
[UIView animateKeyframesWithDuration:duration delay:delay options:options animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(floorf(index/2) * elementWidth, 0)];
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
}];
} completion:^(BOOL finished) {
//Completion Block
}];
This will get you two updates and of course you could use some math and a loop to add up a lot more of these with the appropriate timings.
On previous iOS versions, you'll have to drop to CoreAnimation for keyframe animations, but it's basically the same thing with a bit different syntax.
Method 2:
You can try polling the presentationLayer of the scrollview for any changes with a timer that you start at the beginning of the animation, since unfortunately the presentationLayer's properties aren't KVO observable. Or you can use needsDisplayForKey in a subclass of the layer to get notified when the bounds change, but that'll require some work to set up and it does cause redrawing, which might affect performance.
Method 3:
Would be to dissect exactly what happens to the scrollView when animated is YES try and intercept the animation that gets set on the scrollview and change its parameters, but since this would be the most hacky, breakable due to Apple's changes and trickiest method, I won't go into it.
A nice way to do this is with the AnimationEngine library. It's a very small library: six files, with three more if you want damped spring behavior.
Behind the scenes it uses a CADisplayLink to run your animation block once every frame. You get a clean block-based syntax that's easy to use, and a bunch of interpolation and easing functions that save you time.
To animate contentOffset:
startOffset = scrollView.contentOffset;
endOffset = ..
// Constant speed looks good...
const CGFloat kTimelineAnimationSpeed = 300;
CGFloat timelineAnimationDuration = fabs(deltaToDesiredX) / kTimelineAnimationSpeed;
[INTUAnimationEngine animateWithDuration:timelineAnimationDuration
delay:0
easing:INTULinear
animations:^(CGFloat progress) {
self.videoTimelineView.contentOffset =
INTUInterpolateCGPoint(startOffset, endOffset, progress);
}
completion:^(BOOL finished) {
autoscrollEnabled = YES;
}];
Try this:
UIView.animate(withDuration: 0.6, animations: {
self.view.collectionView.contentOffset = newOffset
self.view.layoutIfNeeded()
}, completion: nil)

Smooth frame animating on UIView

I am trying to move a UIView around the screen by incrementing the UIView's x property in an animation block. I want the element to move continuously so I cannot just specify an ending x and up the duration.
This code works but it is very choppy. Looks great in the simulator but choppy on the device.
-(void)moveGreyDocumentRight:(UIImageView*)greyFolderView
{
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
NSInteger newX = greyFolderView.frame.origin.x + 5.0;
greyFolderView.frame = CGRectMake(newX, greyFolderView.frame.origin.y, greyFolderView.frame.size.width, greyFolderView.frame.size.height);
}
} completion:^(BOOL finished) {
[self moveGreyDocumentRight:greyFolderView];
}];
}
You're fighting the view animation here. Each one of your animations includes a UIViewAnimationOptionCurveEaseInOut timing curve. That means that every 0.05 seconds you try to ramp up your speed then slow down your speed then change to somewhere else.
The first and simplest solution is likely to change to a linear timing by passing the option UIViewAnimationOptionCurveLinear.
That said, making a new animation every 5ms really fights the point of Core Animation, complicating the code and hurting performance. Send the frame it to the place you currently want it to go. Whenever you want it to go somewhere else (even if it's still animating), send it to the new place passing the option UIViewAnimationOptionBeginFromCurrentState. It will automatically adjust to the new target. If you want it to repeat the animation or bounce back and forth, use the repeating options (UIViewAnimationOptionRepeat and UIViewAnimationOptionAutoreverse).

Unexpected behaviour with UIViewAnimationOptionBeginFromCurrentState

I'm trying to implement multistage animation using UIViewAnimationOptionBeginFromCurrentState to allow the user to cancel the animation at will. The animation is a view that continually and cyclically animates between two sizes. When the user touches the view to cancel the animation, I want the view to quickly revert back to its original, small size, whether it was growing or shrinking at the time.
I'm implementing the multistaging aspect by having two separate animations, one for growing the view and one for shrinking it. Each calls the other routine in its completion block, thus cycling forever unless the abort flag has been set.
I get the expected behaviour if abort is called during the grow-the-view animation: the animation quickly and immediately returns the view to its original, small size and stops. Good!
However, if abort is called during the shrink-the-view animation cycle, the view continues to shrink at the same speed (and then stops as expected), as if the UIViewAnimationOptionBeginFromCurrentState option was never invoked.
Code will hopefully make this clearer and hopefully somebody can see what I can't.
- (void)stopAnimating {
abort = YES;
[UIView animateWithDuration:.2 // some small interval
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{ self.frame = minRect;}
completion:^(BOOL done){}
];
}
- (void)animateSmall {
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{self.frame = minRect;}
completion:^(BOOL done){if (!abort)[self animateBig];}
];
}
- (void)animateBig {
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{self.frame = maxRect;}
completion:^(BOOL done){if (!abort)[self animateSmall];}
];
}
Just a guess here, because your code looks exactly as I would have done it. But I think what's going on is that the abort animation is setting the same attribute to the same value as the animation it's interrupting, and this gets treated as equivalent and not in need of change (even though the duration changes).
A test of this theory - and a fix to the problem - would be to make your oscillating minRect just a little bit different in size than your steady state minRect.
Hope this works. Good luck.

ios MapKit MKOverlayView animation happening instantly

I am trying to animate the alpha value of a MapKit overlay view (specifically an MKCircleView) in iOS 5 using the following code:
-(void) animateCircle:(MKCircle*)circle onMap:(MKMapView*) mapView
{
MKCircleView * circleView = (MKCircleView*) [mapView viewForOverlay:circle];
UIViewAnimationOptions options = UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionTransitionNone;
[UIView animateWithDuration:5.0
delay:0.0
options:options
animations:^(void) { circleView.alpha = 0.9; }
completion:^(BOOL finished) {}
];
}
The alpha value of the overlay is changing as I want, but it is jumping there instantaneously rather than animating over the specified duration.
Can anyone suggest what might be wrong? Perhaps animation on overlay views os more complex with blocks than I had thought.
Core Animation has interesting behavior when concurrent animations effect the same view... If you try to animate a view before the view's last animation finished, it will assume you intended the subsequent animation to start from the desired end-state of the initial one. This can result in jumps of frames as well as jumps of alpha values.
In your case, this view is likely being animated by something else. Try locating and removing the other animation / or'ing in UIViewAnimationOptionBeginFromCurrentState to its options.

Resources