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)
This question already has answers here:
How to make UIView animation sequence repeat and autoreverse
(3 answers)
Closed 9 years ago.
I've written the following code to make my UIView constantly fade in and out. (FadeAlphaValue is a BOOL)...
-(void) fade {
[UIView animateWithDuration:1.0
animations:^{
fadeView.alpha = (int)fadeAlphaValue;
}
completion:^(BOOL finished){
fadeAlphaValue=!fadeAlphaValue;
[self fade];
}];
}
It works but I have a feeling it's going to cause some weird crash if I let it run forever... I'm not to familiar with [..^{..} completion^{...}]; that notation. And I feel like since I'm calling the "fade" function during completion it won't actually complete until the "fade" function completes, the problem is the fade function will call itself again before it completes and so on and so on, it seems like an infinite loop... Is this going to cause some kind of weird multi-threading freeze after a few hundred iterations?
A better approach to this is to use the UIViewAnimationOptionRepeat option which will repeat an animation indefinitely.
[UIView animateWithDuration:1.0f
delay:0.0f
options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
animations:^{
// your animation code here
}
completion:nil];//NOTE - this REPEAT animation STOPS if you exit the app (or viewcontroller)... so you must call it again (and reset all animation variables (e.g. alpha) in the UIApplicationDidBecomeActiveNotification function as well (or when the viewcontroller becomes active again)!
I have method -(void)animationStart that consist a lot of [self performSelector:#selector(myAnimation) withObject:nil afterDelay: 21 * 0.01]; with different method's and delays.
How can I refactor my code not to use performSelector? I use it for make animation consequentially change.
You don't specify how you exactly perform the animations themselves, but with UIView animation blocks you can also specify a delay.(animateWithDuration:delay:options:animations:completion:).
Alternatively you can use NSTimerfor performing delays.
But for a detailed answer you should provide more details on how you perform your animation.
Update
Using animation blocks is very straightforward...In the completion block (see code below) you can start the follow up animation (for series of animations). For parallel animation processing you just start multiple blocks of animations...there are different messages. If you don't need to handle the animation end, you can use the ones without completion block.
Here is an example:
[UIView animateWithDuration:2.0 delay:0.1 options:UIViewAnimationOptionCurveEaseInOut animations:^{
// perform animation code here
} completion:^(BOOL finished) {
// This code is performed after the animation is completed. E.g. you can start a new animation here
// (for serial animation processing):
}];
I'm trying to animate some stuff using the usual UIView class methods. I'm calling these animate methods in a method called by the popular iCarousel library. When I scroll the carousel, the animation immediately goes to its completion state and calls the completion function (ignoring the duration on the animation). The finished BOOL is true, so it doesn't look like the animation is being interrupted.
When I trigger these animations outside of iCarousel's delegate methods, they work. I'm not looking to fix iCarousel, nor do I presume something is broken with it. I'm more curious what code inside could possibly be causing my animation to complete immediately. And what I can do to avoid the problem.
Here's an example:
- (void)carouselCurrentItemIndexDidChange:(iCarousel *)carousel {
[UIView
animateWithDuration:5.0f
delay:0
options:
UIViewAnimationOptionCurveEaseOut |
UIViewAnimationOptionOverrideInheritedDuration |
UIViewAnimationOptionOverrideInheritedCurve
animations:^{
myView.alpha = 0;
}
completion:^(BOOL finished){
NSLog(#"Completed. Did it finish? %i", finished);
}];
}
Last detail:
iCarousel's documentation mentions that you can change the useDisplayLink property of the carousel to avoid some conflicts with animation. This doesn't seem to make any difference.
Thanks in advance for help!
I found the problem deep within the source of iCarousel. For some reason within their library, they disable animations using [CATransaction setDisableActions:YES];, then reenable it later in a bunch of different places. One such places straddles a bunch of their callbacks which is preventing my animations from firing properly.
Judging by the iCarousel code, this is very intentional, so I think I'll have to work around the issue in my own code.
Edit: Here's my updated code that avoids this issue:
- (void)carouselDidEndScrollingAnimation:(iCarousel *)carousel {
BOOL previousDisableActions = [CATransaction disableActions];
[CATransaction setDisableActions:NO];
// Begin animation here.
[CATransaction setDisableActions:previousDisableActions];
}
Update: The author of iCarousel has fixed this bug by internally adding the code I suggested above around each delegate call. Looks like this is fixed for good.
Why does this not work?
[UIView animateWithDuration:0 delay:3 options:0 animations:^(void) {
NSLog(#"after duration, please!");
} completion:nil];
The NSLog fires immediately.
I'm not looking for workarounds (there are many), but rather wondering why this would be.
The intermediate frames of core animation are actually happening in another background thread. The first and last frame of the animation are often created immediately. So the NSLog can fire in the main UI thread without waiting or even knowing about any background animation in progress.